* [PATCH v8 00/12] ZTE zx297520v3 clock bindings and driver
@ 2026-07-27 18:24 ` Stefan Dösinger
0 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger, Krzysztof Kozlowski
Hi,
I am sending version 8 of my zx297520v3 clock patch. The main change vs
v6 is the reverse engineering of the PLL input and forward gates in
top+0x140 and according reduction of the input clocks in the matrix
binding.
I think the list of clocks in my driver is fairly complete; It is
certainly a lot better than what the downstream ZTE drivers have. I
deduced a lot of it by trial and error. I am sure there are some clocks
missing that will need to be added to the binding later. Afaiu adding
clocks is not an issue, but removing or reordering them is an ABI break.
Merge dependencies: The binding patches (1, 2, 3) and DT patch (12) in
order. The MFD parent (4) is independent, although changing the ordering
likely needs trivial adjustments in MAINTAINERS. The clock driver
patches depend on the headers added by 1-3 and apply in order
(5-10). Reset needs headers (1-3), but it is independent of clock.
Though in practice mfd, clock and reset drivers are dead code until
patch 12 adds the DT nodes. Clock and reset drivers won't be
instantiated without the MFD parent.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
Changes in v8:
*) Put #phy-cells into topcrm. The mfd child will have to assign itself
the MFD parent's of_node to satisfy the check in phy-core.c's
__of_phy_provider_register().
*) Added another register to matrixclk that controls in-controller
distribution gates. No functional change, but it nicely confirms the
top->matrix interface
*) Changed clock indices to start at 0, adjusted driver to not use 0 as
"do not export" marker.
*) Rename DT fixed clock nodes from osc26m -> clock-26m and osc32k ->
clock-32k
*) Fixed more issues pointed out by Sashiko
- Link to v7: https://lore.kernel.org/r/20260717-zx29clk-v7-0-408411cfcf36@gmail.com
Changes in v7:
*) Moved DT bindings back to clock/ (Krzysztof)
*) Added a usb-phy child node to top to expose the USB status register
*) Add clock gates that control input of PLL clocks into topcrm and
forward to matrixcrm. Strip down matrixcrm inputs in the bindings to the
known clocks in this register
*) Add DDR clocks and resets, arm arch timer clock. They are marked
critical and tell CCF not to gate off the aforementioned input/forward
gates
*) Remove syscon from matrixcrm (Krzysztof). The DDR mailbox system is
on a different controller (soc_sys at 0x140000). ZTE's code merely
controlled clocks in matrixcrm, which doesn't require syscon on this
controller.
*) Consistently use double quotes in bindings and full include paths
(Krzysztof)
The individual patches call out changes in more detail.
- Link to v6: https://lore.kernel.org/r/20260702-zx29clk-v6-0-377b704f80c4@gmail.com
Changes in v6:
*) Use MFD for all 3 controllers - I hope both Conor and Philipp will
agree. I kept top and matrix bindings in soc/zte and lsp in clock/
though.
*) Clean up issues found by Sashiko. I pointed them out in the individual
patches. They are localized fixes and don't affect the overall design
*) Small code consistency: Changed "zx297520v3_lsp" to use "-" , "rst"
in driver names to "reset"
Changes in v5:
*) Use MFD instead of aux bus for top and matrix clocks
*) Move top and matrix bindings to soc/zte
*) Give USB PHY its own resets
*) Other localized changes are noted in the individual patches
- Link to v4: https://lore.kernel.org/r/20260616-zx29clk-v4-0-ca994bd22e9d@gmail.com
Changes in v4:
*) Use syscon and regmap instead of raw IO
*) Move reset to its own driver on the aux bus, but keep reset and clk
in the same binding as it matches the way the hardware works
*) Go back to having matrixclk in its own device because syscon deals
poorly with multi io reg devices. List all PLL outputs from topclk as
inputs to matrixclk
*) Some more hardware research: Figure out the parents of the 4 possible
GPIO clock outputs and declare them in the driver. They are unused on
the hardware I have, but they show that all PLLs can be used.
- Link to v3: https://lore.kernel.org/r/20260529-zx29clk-v3-0-c7fe54ea388f@gmail.com
Changes in v3:
Model top and matrix clocks as one device
Add PLL driver
Fixed a few issues found by Sashiko: register lock, some missing devm_,
error handling
v2: Fix build issues introduced by checkpatch.pl fixes that I didn't
spot earlier.
---
Stefan Dösinger (12):
dt-bindings: clk: zte: Add zx297520v3 top clock and reset controller
dt-bindings: clk: zte: Add zx297520v3 matrix clock and reset controller
dt-bindings: clk: zte: Add zx297520v3 LSP clock and reset controller
mfd: zx297520v3: Add a clock and reset MFD driver
clk: zte: Add Clock registration infrastructure
clk: zte: Add regmap-based clocks
clk: zte: Add zx PLL support infrastructure
clk: zte: Introduce a driver for zx297520v3 top clocks
clk: zte: Introduce a driver for zx297520v3 matrix clocks
clk: zte: Introduce a driver for zx297520v3 LSP clocks
reset: zte: Add a zx297520v3 reset driver
ARM: dts: zte: Declare zx297520v3 CRM device nodes
.../bindings/clock/zte,zx297520v3-lspcrm.yaml | 101 ++
.../bindings/clock/zte,zx297520v3-matrixcrm.yaml | 96 ++
.../bindings/clock/zte,zx297520v3-topcrm.yaml | 136 +++
MAINTAINERS | 9 +
arch/arm/boot/dts/zte/zx297520v3.dtsi | 84 +-
drivers/clk/Kconfig | 1 +
drivers/clk/Makefile | 1 +
drivers/clk/zte/Kconfig | 27 +
drivers/clk/zte/Makefile | 6 +
drivers/clk/zte/clk-regmap.c | 246 +++++
drivers/clk/zte/clk-zx.c | 155 +++
drivers/clk/zte/clk-zx.h | 91 ++
drivers/clk/zte/clk-zx297520v3.c | 1103 ++++++++++++++++++++
drivers/clk/zte/pll-zx.c | 560 ++++++++++
drivers/mfd/Kconfig | 11 +
drivers/mfd/Makefile | 2 +
drivers/mfd/zte-zx297520v3-crm.c | 98 ++
drivers/reset/Kconfig | 12 +
drivers/reset/Makefile | 1 +
drivers/reset/reset-zte-zx297520v3.c | 237 +++++
include/dt-bindings/clock/zte,zx297520v3-clk.h | 144 +++
include/dt-bindings/phy/zte,zx297520v3-topcrm.h | 12 +
include/dt-bindings/reset/zte,zx297520v3-reset.h | 63 ++
23 files changed, 3188 insertions(+), 8 deletions(-)
---
base-commit: bee763d5f341b99cf472afeb508d4988f62a6ca1
change-id: 20260510-zx29clk-2e4d39e3128c
Best regards,
--
Stefan Dösinger <stefandoesinger@gmail.com>
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 50+ messages in thread* [PATCH v8 01/12] dt-bindings: clk: zte: Add zx297520v3 top clock and reset controller
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 18:24 ` Stefan Dösinger
-1 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
These SoCs have 3 clock and reset controllers: Top, Matrix and LSP.
Clocks go from oscillator -> top -> matrix -> LSP, with a register in
top controlling most clocks that get passed to matrix and a register in
matrix controlling all the clocks that get passed to LSP.
Generally every device has two clocks (one work clock, and one that
connects it to the bus, I call it PCLK), two reset bits (I don't know
what the difference is - sometimes asserting one is enough to reset the
device, sometimes both need to be asserted). PCLK and WCLK are
controlled by individual gates. Some devices have a mux and/or a
divider for their work clock. Some devices, like the GPIO controller,
only have reset bits and no clocks.
The top clock controller is fed by a 26 MHz external oscillator and has 4
PLLs to generate other clock rates. ZTE's kernel mostly relies on the
boot ROM to set up PLLs, but one LTE-related PLL is not configured
on some boards. Therefore my driver contains code to program PLLs. It
produces identical settings as the boot ROM for the pre-programmed
frequencies.
Not all clocks will have an explicit user in the end. I am defining a
lot of them simply to shut them off. The boot loader sets up a few of
the proprietary timers, which will send regular IRQs (although the
kernel of course doesn't need to listen to them). I don't plan to add a
driver for the proprietary timer as I see no use for them - the ARM arch
timer works just fine. I will add a driver for the very similar
proprietary watchdog though.
The clock list in this patch is pretty complete but not exhaustive.
There are other bits that are enabled, but I couldn't deduce what they
are controlling by trial and error. Some of them seem to do nothing.
Others cause an instant hang of the board when disabled. It is quite
likely that a handful more clocks will be added in the future, but not a
large number.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
Changes v7->v8 (all Krzysztof):
*) Fold the PHY into the top-level node
*) Start clock indices at 0
*) Rename the phy include file to zte,zx297520v3-topcrm.h.
And a non-change despite/because of new discoveries: USB has more PHY
and DWC controls in a different controller at 0x140000. ZTE calls this
controller SOC_SYS, and it is a textbook case for syscon: It has a lot
of different controls: A remap flag for boot ROM, a control to bring the
A53 up in arm32 vs aarch64 mode, the entrypoint of the LTE DSP, SDIO,
I2S, ...
USB-wise it has a register with PHY and DWC controls: Endianness for the
DWC2 registers, PHY clock adjustments, PHY and DWC host/device switch.
In spite of this I think topcrm is the better home for #phy-cells: The
USB ready flags in topcrm are essential for bringing up the USB devices
whereas the soc_sys controls are a curiosity. Host mode only works if I
externally inject VBUS. Clock adjustment is too inflexible to be of real
value. Flipping the DWC registers to big endian works, but what for? If
I find a use case for changing the default USB values in soc_sys it can
be handled via syscon.
soc_sys also has power controls for a few devices, including USB. This
will be handled via #power-domain-cells.
The self-referencing reset property causes the following boot messages:
/soc/clock-controller@13b000: Fixed dependency cycle(s) with
/soc/clock-controller@13b000
/soc/clock-controller@13b000: Fixed dependency cycle(s) with
/soc/clock-controller@13b000
Do I need to add post-init-providers? As far as I understand it, this
message is informational and not a problem that needs to be addressed.
Changes v6->v7:
*) Moved the bindings back into clock/ (Krzysztof). Use clock-controller@
in example and in the later DTSI patch.
*) With a lucky find in strings in LTE's cpko.ko blob I could make sense
of the register that controls which clocks get passed from PLLs into
the clock distribution inside topcrm. Importantly that also narrows
which clocks can possibly be passed to Matrix, so I could slim down the
bindings considerably.
*) Add a usb phy child node with the phy schema added in the previous
patch. I don't expect the USB status register to ever be in another
place, so the binding expresses the @84 expectation.
Changes v5->v6:
Set value for syscon-reboot example (Sashiko). It was my intention to
set only the lowest bit, and I think Sashiko is right that without
'value' being set, all other bits are actively set to 0. It shouldn't
matter given my understanding of the hardware (afaics all other bits are
ignored), but actively clearing bits was not my intention.
I haven't changed the name match for "syscon-reboot". I see plenty of
examples of hardcoding this string as opposed to having a regex for
syscon-reboot@12345678 in other bindings.
Changes v4->v5:
Rename from zte,zx297520v3-topclk to zte,zx297520v3-topcrm and move to
soc/zte
Fix path in MAINTAINERS
Add syscon-reboot node to the binding
Give the USB and HSIC PHY resets their own reset control
---
.../bindings/clock/zte,zx297520v3-topcrm.yaml | 136 +++++++++++++++++++++
MAINTAINERS | 4 +
include/dt-bindings/clock/zte,zx297520v3-clk.h | 66 ++++++++++
include/dt-bindings/phy/zte,zx297520v3-topcrm.h | 12 ++
include/dt-bindings/reset/zte,zx297520v3-reset.h | 32 +++++
5 files changed, 250 insertions(+)
diff --git a/Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml b/Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
new file mode 100644
index 000000000000..9dbca49871a0
--- /dev/null
+++ b/Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
@@ -0,0 +1,136 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/clock/zte,zx297520v3-topcrm.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE zx297520v3 SoC top clock and reset controller
+
+maintainers:
+ - Stefan Dösinger <stefandoesinger@gmail.com>
+
+description: |
+ The zx297520v3 top clock and reset controller generates clocks for core
+ devices on the board like the main bus, USB and timers. In addition to clocks
+ it has reset controls for peripherals, a global board reset, watchdog reset
+ controls and a USB status register.
+
+ The controller has two clock inputs: a 26 MHz and a 32 kHz external
+ oscillator. They need to be provided as input clocks. The controller provides
+ PLL output frequencies to downstream clock controllers.
+
+ The USB PHY is exposed as two PHYs, one for USB and another for HSIC. Note
+ that while the controls for both PHYs are always present, the PHYs themselves
+ may be absent. If this is the case the controller never signals that the
+ missing PHY is ready.
+
+ All available clocks are defined as preprocessor macros in the
+ "include/dt-bindings/clock/zte,zx297520v3-clk.h" header. The resets are
+ defined in the "include/dt-bindings/reset/zte,zx297520v3-reset.h" header. PHY
+ defines are found in "include/dt-bindings/phy/zte,zx297520v3-topcrm.h".
+
+properties:
+ compatible:
+ items:
+ - const: zte,zx297520v3-topcrm
+ - const: syscon
+
+ reg:
+ maxItems: 1
+
+ clocks:
+ items:
+ - description: 26 MHz external oscillator
+ - description: 32 kHz external oscillator
+
+ clock-names:
+ items:
+ - const: osc26m
+ - const: osc32k
+
+ interrupts:
+ items:
+ - description: IRQ reporting USB connection
+ - description: IRQ reporting USB disconnection
+ - description: IRQ reporting HSIC connection
+ - description: IRQ reporting HSIC disconnection
+
+ interrupt-names:
+ items:
+ - const: usb-up
+ - const: usb-down
+ - const: hsic-up
+ - const: hsic-down
+
+ resets:
+ items:
+ - description: USB phy reset
+ - description: HSIC phy reset
+
+ reset-names:
+ items:
+ - const: usb
+ - const: hsic
+
+ "#clock-cells":
+ const: 1
+
+ "#phy-cells":
+ const: 1
+
+ "#reset-cells":
+ const: 1
+
+ syscon-reboot:
+ type: object
+ $ref: /schemas/power/reset/syscon-reboot.yaml#
+ description:
+ Reboot method for the SoC.
+
+required:
+ - compatible
+ - reg
+ - clocks
+ - clock-names
+ - interrupts
+ - interrupt-names
+ - resets
+ - reset-names
+ - syscon-reboot
+ - "#clock-cells"
+ - "#phy-cells"
+ - "#reset-cells"
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+ #include <dt-bindings/reset/zte,zx297520v3-reset.h>
+ #include <dt-bindings/phy/zte,zx297520v3-topcrm.h>
+ #include <dt-bindings/clock/zte,zx297520v3-clk.h>
+
+ topcrm: clock-controller@13b000 {
+ compatible = "zte,zx297520v3-topcrm", "syscon";
+ reg = <0x0013b000 0x400>;
+ clocks = <&osc26m>, <&osc32k>;
+ clock-names = "osc26m", "osc32k";
+ interrupts = <GIC_SPI 42 IRQ_TYPE_EDGE_RISING>,
+ <GIC_SPI 43 IRQ_TYPE_EDGE_RISING>,
+ <GIC_SPI 45 IRQ_TYPE_EDGE_RISING>,
+ <GIC_SPI 46 IRQ_TYPE_EDGE_RISING>;
+ interrupt-names = "usb-up", "usb-down", "hsic-up", "hsic-down";
+ resets = <&topcrm ZX297520V3_USB_PHY_RESET>,
+ <&topcrm ZX297520V3_HSIC_PHY_RESET>;
+ reset-names = "usb", "hsic";
+ #clock-cells = <1>;
+ #phy-cells = <1>;
+ #reset-cells = <1>;
+
+ syscon-reboot {
+ compatible = "syscon-reboot";
+ offset = <0x0>;
+ mask = <0x1>;
+ value = <0x1>;
+ };
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index 8729cea57c3d..36619a9ff8fc 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3878,8 +3878,12 @@ L: linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
S: Odd fixes
F: Documentation/arch/arm/zte/
F: Documentation/devicetree/bindings/arm/zte.yaml
+F: Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
F: arch/arm/boot/dts/zte/
F: arch/arm/mach-zte/
+F: include/dt-bindings/clock/zte,zx297520v3-clk.h
+F: include/dt-bindings/phy/zte,zx297520v3-topcrm.h
+F: include/dt-bindings/reset/zte,zx297520v3-reset.h
ARM/ZYNQ ARCHITECTURE
M: Michal Simek <michal.simek@amd.com>
diff --git a/include/dt-bindings/clock/zte,zx297520v3-clk.h b/include/dt-bindings/clock/zte,zx297520v3-clk.h
new file mode 100644
index 000000000000..3aed94ccc26f
--- /dev/null
+++ b/include/dt-bindings/clock/zte,zx297520v3-clk.h
@@ -0,0 +1,66 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
+/*
+ * Copyright (C) Stefan Dösinger.
+ */
+
+#ifndef __DT_BINDINGS_CLOCK_ZX297520V3_H
+#define __DT_BINDINGS_CLOCK_ZX297520V3_H
+
+#define ZX297520V3_M0_WCLK 0
+#define ZX297520V3_SRAM1_PCLK 1
+#define ZX297520V3_SRAM2_PCLK 2
+#define ZX297520V3_UART0_WCLK 3
+#define ZX297520V3_UART0_PCLK 4
+#define ZX297520V3_I2C0_WCLK 5
+#define ZX297520V3_I2C0_PCLK 6
+#define ZX297520V3_RTC_WCLK 7
+#define ZX297520V3_RTC_PCLK 8
+#define ZX297520V3_LPM_GSM_WCLK 9
+#define ZX297520V3_LPM_GSM_PCLK 10
+#define ZX297520V3_LPM_LTE_WCLK 11
+#define ZX297520V3_LPM_LTE_PCLK 12
+#define ZX297520V3_LPM_TD_WCLK 13
+#define ZX297520V3_LPM_TD_PCLK 14
+#define ZX297520V3_LPM_W_WCLK 15
+#define ZX297520V3_LPM_W_PCLK 16
+#define ZX297520V3_TIMER_T08_WCLK 17
+#define ZX297520V3_TIMER_T08_PCLK 18
+#define ZX297520V3_TIMER_T09_WCLK 19
+#define ZX297520V3_TIMER_T09_PCLK 20
+#define ZX297520V3_MPLL 21
+#define ZX297520V3_MPLL_D5 22
+#define ZX297520V3_DPLL 23
+#define ZX297520V3_GPLL 24
+#define ZX297520V3_GPLL_D2 25
+#define ZX297520V3_GATED_OSC26M 26
+#define ZX297520V3_PMM_WCLK 27
+#define ZX297520V3_PMM_PCLK 28
+#define ZX297520V3_OUT0_WCLK 29
+#define ZX297520V3_OUT1_WCLK 30
+#define ZX297520V3_OUT2_WCLK 31
+#define ZX297520V3_OUT32K_WCLK 32
+#define ZX297520V3_RMIIPHY_WCLK 33
+#define ZX297520V3_TIMER_T12_WCLK 34
+#define ZX297520V3_TIMER_T12_PCLK 35
+#define ZX297520V3_TIMER_T13_WCLK 36
+#define ZX297520V3_TIMER_T13_PCLK 37
+#define ZX297520V3_TIMER_T14_WCLK 38
+#define ZX297520V3_TIMER_T14_PCLK 39
+#define ZX297520V3_TIMER_T15_WCLK 40
+#define ZX297520V3_TIMER_T15_PCLK 41
+#define ZX297520V3_TIMER_T16_WCLK 42
+#define ZX297520V3_TIMER_T16_PCLK 43
+#define ZX297520V3_TIMER_T17_WCLK 44
+#define ZX297520V3_TIMER_T17_PCLK 45
+#define ZX297520V3_WDT_T18_WCLK 46
+#define ZX297520V3_WDT_T18_PCLK 47
+#define ZX297520V3_USIM1_WCLK 48
+#define ZX297520V3_USIM1_PCLK 49
+#define ZX297520V3_AHB_WCLK 50
+#define ZX297520V3_AHB_PCLK 51
+#define ZX297520V3_USB_WCLK 52
+#define ZX297520V3_USB_PCLK 53
+#define ZX297520V3_HSIC_WCLK 54
+#define ZX297520V3_HSIC_PCLK 55
+
+#endif /* __DT_BINDINGS_CLOCK_ZX297520V3_H */
diff --git a/include/dt-bindings/phy/zte,zx297520v3-topcrm.h b/include/dt-bindings/phy/zte,zx297520v3-topcrm.h
new file mode 100644
index 000000000000..b36ccf2f54ef
--- /dev/null
+++ b/include/dt-bindings/phy/zte,zx297520v3-topcrm.h
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
+/*
+ * Copyright (C) Stefan Dösinger.
+ */
+
+#ifndef __DT_BINDINGS_PHY_ZTE_ZX297520V3_TOPCRM_H
+#define __DT_BINDINGS_PHY_ZTE_ZX297520V3_TOPCRM_H
+
+#define ZX297520V3_USB_PHY 0
+#define ZX297520V3_HSIC_PHY 1
+
+#endif /* __DT_BINDINGS_PHY_ZTE_ZX297520V3_TOPCRM_H */
diff --git a/include/dt-bindings/reset/zte,zx297520v3-reset.h b/include/dt-bindings/reset/zte,zx297520v3-reset.h
new file mode 100644
index 000000000000..43db72bb59de
--- /dev/null
+++ b/include/dt-bindings/reset/zte,zx297520v3-reset.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
+/*
+ * Copyright (C) Stefan Dösinger.
+ */
+
+#ifndef __DT_BINDINGS_RESET_ZX297520V3_H
+#define __DT_BINDINGS_RESET_ZX297520V3_H
+
+#define ZX297520V3_ZSP_RESET 0
+#define ZX297520V3_UART0_RESET 1
+#define ZX297520V3_I2C0_RESET 2
+#define ZX297520V3_RTC_RESET 3
+#define ZX297520V3_TIMER_T08_RESET 4
+#define ZX297520V3_TIMER_T09_RESET 5
+#define ZX297520V3_PMM_RESET 6
+#define ZX297520V3_GPIO_RESET 7
+#define ZX297520V3_GPIO8_RESET 8
+#define ZX297520V3_TIMER_T12_RESET 9
+#define ZX297520V3_TIMER_T13_RESET 10
+#define ZX297520V3_TIMER_T14_RESET 11
+#define ZX297520V3_TIMER_T15_RESET 12
+#define ZX297520V3_TIMER_T16_RESET 13
+#define ZX297520V3_TIMER_T17_RESET 14
+#define ZX297520V3_WDT_T18_RESET 15
+#define ZX297520V3_USIM1_RESET 16
+#define ZX297520V3_AHB_RESET 17
+#define ZX297520V3_USB_PHY_RESET 18
+#define ZX297520V3_USB_RESET 19
+#define ZX297520V3_HSIC_PHY_RESET 20
+#define ZX297520V3_HSIC_RESET 21
+
+#endif /* __DT_BINDINGS_RESET_ZX297520V3_H */
--
2.54.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* [PATCH v8 01/12] dt-bindings: clk: zte: Add zx297520v3 top clock and reset controller
@ 2026-07-27 18:24 ` Stefan Dösinger
0 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
These SoCs have 3 clock and reset controllers: Top, Matrix and LSP.
Clocks go from oscillator -> top -> matrix -> LSP, with a register in
top controlling most clocks that get passed to matrix and a register in
matrix controlling all the clocks that get passed to LSP.
Generally every device has two clocks (one work clock, and one that
connects it to the bus, I call it PCLK), two reset bits (I don't know
what the difference is - sometimes asserting one is enough to reset the
device, sometimes both need to be asserted). PCLK and WCLK are
controlled by individual gates. Some devices have a mux and/or a
divider for their work clock. Some devices, like the GPIO controller,
only have reset bits and no clocks.
The top clock controller is fed by a 26 MHz external oscillator and has 4
PLLs to generate other clock rates. ZTE's kernel mostly relies on the
boot ROM to set up PLLs, but one LTE-related PLL is not configured
on some boards. Therefore my driver contains code to program PLLs. It
produces identical settings as the boot ROM for the pre-programmed
frequencies.
Not all clocks will have an explicit user in the end. I am defining a
lot of them simply to shut them off. The boot loader sets up a few of
the proprietary timers, which will send regular IRQs (although the
kernel of course doesn't need to listen to them). I don't plan to add a
driver for the proprietary timer as I see no use for them - the ARM arch
timer works just fine. I will add a driver for the very similar
proprietary watchdog though.
The clock list in this patch is pretty complete but not exhaustive.
There are other bits that are enabled, but I couldn't deduce what they
are controlling by trial and error. Some of them seem to do nothing.
Others cause an instant hang of the board when disabled. It is quite
likely that a handful more clocks will be added in the future, but not a
large number.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
Changes v7->v8 (all Krzysztof):
*) Fold the PHY into the top-level node
*) Start clock indices at 0
*) Rename the phy include file to zte,zx297520v3-topcrm.h.
And a non-change despite/because of new discoveries: USB has more PHY
and DWC controls in a different controller at 0x140000. ZTE calls this
controller SOC_SYS, and it is a textbook case for syscon: It has a lot
of different controls: A remap flag for boot ROM, a control to bring the
A53 up in arm32 vs aarch64 mode, the entrypoint of the LTE DSP, SDIO,
I2S, ...
USB-wise it has a register with PHY and DWC controls: Endianness for the
DWC2 registers, PHY clock adjustments, PHY and DWC host/device switch.
In spite of this I think topcrm is the better home for #phy-cells: The
USB ready flags in topcrm are essential for bringing up the USB devices
whereas the soc_sys controls are a curiosity. Host mode only works if I
externally inject VBUS. Clock adjustment is too inflexible to be of real
value. Flipping the DWC registers to big endian works, but what for? If
I find a use case for changing the default USB values in soc_sys it can
be handled via syscon.
soc_sys also has power controls for a few devices, including USB. This
will be handled via #power-domain-cells.
The self-referencing reset property causes the following boot messages:
/soc/clock-controller@13b000: Fixed dependency cycle(s) with
/soc/clock-controller@13b000
/soc/clock-controller@13b000: Fixed dependency cycle(s) with
/soc/clock-controller@13b000
Do I need to add post-init-providers? As far as I understand it, this
message is informational and not a problem that needs to be addressed.
Changes v6->v7:
*) Moved the bindings back into clock/ (Krzysztof). Use clock-controller@
in example and in the later DTSI patch.
*) With a lucky find in strings in LTE's cpko.ko blob I could make sense
of the register that controls which clocks get passed from PLLs into
the clock distribution inside topcrm. Importantly that also narrows
which clocks can possibly be passed to Matrix, so I could slim down the
bindings considerably.
*) Add a usb phy child node with the phy schema added in the previous
patch. I don't expect the USB status register to ever be in another
place, so the binding expresses the @84 expectation.
Changes v5->v6:
Set value for syscon-reboot example (Sashiko). It was my intention to
set only the lowest bit, and I think Sashiko is right that without
'value' being set, all other bits are actively set to 0. It shouldn't
matter given my understanding of the hardware (afaics all other bits are
ignored), but actively clearing bits was not my intention.
I haven't changed the name match for "syscon-reboot". I see plenty of
examples of hardcoding this string as opposed to having a regex for
syscon-reboot@12345678 in other bindings.
Changes v4->v5:
Rename from zte,zx297520v3-topclk to zte,zx297520v3-topcrm and move to
soc/zte
Fix path in MAINTAINERS
Add syscon-reboot node to the binding
Give the USB and HSIC PHY resets their own reset control
---
.../bindings/clock/zte,zx297520v3-topcrm.yaml | 136 +++++++++++++++++++++
MAINTAINERS | 4 +
include/dt-bindings/clock/zte,zx297520v3-clk.h | 66 ++++++++++
include/dt-bindings/phy/zte,zx297520v3-topcrm.h | 12 ++
include/dt-bindings/reset/zte,zx297520v3-reset.h | 32 +++++
5 files changed, 250 insertions(+)
diff --git a/Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml b/Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
new file mode 100644
index 000000000000..9dbca49871a0
--- /dev/null
+++ b/Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
@@ -0,0 +1,136 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/clock/zte,zx297520v3-topcrm.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE zx297520v3 SoC top clock and reset controller
+
+maintainers:
+ - Stefan Dösinger <stefandoesinger@gmail.com>
+
+description: |
+ The zx297520v3 top clock and reset controller generates clocks for core
+ devices on the board like the main bus, USB and timers. In addition to clocks
+ it has reset controls for peripherals, a global board reset, watchdog reset
+ controls and a USB status register.
+
+ The controller has two clock inputs: a 26 MHz and a 32 kHz external
+ oscillator. They need to be provided as input clocks. The controller provides
+ PLL output frequencies to downstream clock controllers.
+
+ The USB PHY is exposed as two PHYs, one for USB and another for HSIC. Note
+ that while the controls for both PHYs are always present, the PHYs themselves
+ may be absent. If this is the case the controller never signals that the
+ missing PHY is ready.
+
+ All available clocks are defined as preprocessor macros in the
+ "include/dt-bindings/clock/zte,zx297520v3-clk.h" header. The resets are
+ defined in the "include/dt-bindings/reset/zte,zx297520v3-reset.h" header. PHY
+ defines are found in "include/dt-bindings/phy/zte,zx297520v3-topcrm.h".
+
+properties:
+ compatible:
+ items:
+ - const: zte,zx297520v3-topcrm
+ - const: syscon
+
+ reg:
+ maxItems: 1
+
+ clocks:
+ items:
+ - description: 26 MHz external oscillator
+ - description: 32 kHz external oscillator
+
+ clock-names:
+ items:
+ - const: osc26m
+ - const: osc32k
+
+ interrupts:
+ items:
+ - description: IRQ reporting USB connection
+ - description: IRQ reporting USB disconnection
+ - description: IRQ reporting HSIC connection
+ - description: IRQ reporting HSIC disconnection
+
+ interrupt-names:
+ items:
+ - const: usb-up
+ - const: usb-down
+ - const: hsic-up
+ - const: hsic-down
+
+ resets:
+ items:
+ - description: USB phy reset
+ - description: HSIC phy reset
+
+ reset-names:
+ items:
+ - const: usb
+ - const: hsic
+
+ "#clock-cells":
+ const: 1
+
+ "#phy-cells":
+ const: 1
+
+ "#reset-cells":
+ const: 1
+
+ syscon-reboot:
+ type: object
+ $ref: /schemas/power/reset/syscon-reboot.yaml#
+ description:
+ Reboot method for the SoC.
+
+required:
+ - compatible
+ - reg
+ - clocks
+ - clock-names
+ - interrupts
+ - interrupt-names
+ - resets
+ - reset-names
+ - syscon-reboot
+ - "#clock-cells"
+ - "#phy-cells"
+ - "#reset-cells"
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+ #include <dt-bindings/reset/zte,zx297520v3-reset.h>
+ #include <dt-bindings/phy/zte,zx297520v3-topcrm.h>
+ #include <dt-bindings/clock/zte,zx297520v3-clk.h>
+
+ topcrm: clock-controller@13b000 {
+ compatible = "zte,zx297520v3-topcrm", "syscon";
+ reg = <0x0013b000 0x400>;
+ clocks = <&osc26m>, <&osc32k>;
+ clock-names = "osc26m", "osc32k";
+ interrupts = <GIC_SPI 42 IRQ_TYPE_EDGE_RISING>,
+ <GIC_SPI 43 IRQ_TYPE_EDGE_RISING>,
+ <GIC_SPI 45 IRQ_TYPE_EDGE_RISING>,
+ <GIC_SPI 46 IRQ_TYPE_EDGE_RISING>;
+ interrupt-names = "usb-up", "usb-down", "hsic-up", "hsic-down";
+ resets = <&topcrm ZX297520V3_USB_PHY_RESET>,
+ <&topcrm ZX297520V3_HSIC_PHY_RESET>;
+ reset-names = "usb", "hsic";
+ #clock-cells = <1>;
+ #phy-cells = <1>;
+ #reset-cells = <1>;
+
+ syscon-reboot {
+ compatible = "syscon-reboot";
+ offset = <0x0>;
+ mask = <0x1>;
+ value = <0x1>;
+ };
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index 8729cea57c3d..36619a9ff8fc 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3878,8 +3878,12 @@ L: linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
S: Odd fixes
F: Documentation/arch/arm/zte/
F: Documentation/devicetree/bindings/arm/zte.yaml
+F: Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
F: arch/arm/boot/dts/zte/
F: arch/arm/mach-zte/
+F: include/dt-bindings/clock/zte,zx297520v3-clk.h
+F: include/dt-bindings/phy/zte,zx297520v3-topcrm.h
+F: include/dt-bindings/reset/zte,zx297520v3-reset.h
ARM/ZYNQ ARCHITECTURE
M: Michal Simek <michal.simek@amd.com>
diff --git a/include/dt-bindings/clock/zte,zx297520v3-clk.h b/include/dt-bindings/clock/zte,zx297520v3-clk.h
new file mode 100644
index 000000000000..3aed94ccc26f
--- /dev/null
+++ b/include/dt-bindings/clock/zte,zx297520v3-clk.h
@@ -0,0 +1,66 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
+/*
+ * Copyright (C) Stefan Dösinger.
+ */
+
+#ifndef __DT_BINDINGS_CLOCK_ZX297520V3_H
+#define __DT_BINDINGS_CLOCK_ZX297520V3_H
+
+#define ZX297520V3_M0_WCLK 0
+#define ZX297520V3_SRAM1_PCLK 1
+#define ZX297520V3_SRAM2_PCLK 2
+#define ZX297520V3_UART0_WCLK 3
+#define ZX297520V3_UART0_PCLK 4
+#define ZX297520V3_I2C0_WCLK 5
+#define ZX297520V3_I2C0_PCLK 6
+#define ZX297520V3_RTC_WCLK 7
+#define ZX297520V3_RTC_PCLK 8
+#define ZX297520V3_LPM_GSM_WCLK 9
+#define ZX297520V3_LPM_GSM_PCLK 10
+#define ZX297520V3_LPM_LTE_WCLK 11
+#define ZX297520V3_LPM_LTE_PCLK 12
+#define ZX297520V3_LPM_TD_WCLK 13
+#define ZX297520V3_LPM_TD_PCLK 14
+#define ZX297520V3_LPM_W_WCLK 15
+#define ZX297520V3_LPM_W_PCLK 16
+#define ZX297520V3_TIMER_T08_WCLK 17
+#define ZX297520V3_TIMER_T08_PCLK 18
+#define ZX297520V3_TIMER_T09_WCLK 19
+#define ZX297520V3_TIMER_T09_PCLK 20
+#define ZX297520V3_MPLL 21
+#define ZX297520V3_MPLL_D5 22
+#define ZX297520V3_DPLL 23
+#define ZX297520V3_GPLL 24
+#define ZX297520V3_GPLL_D2 25
+#define ZX297520V3_GATED_OSC26M 26
+#define ZX297520V3_PMM_WCLK 27
+#define ZX297520V3_PMM_PCLK 28
+#define ZX297520V3_OUT0_WCLK 29
+#define ZX297520V3_OUT1_WCLK 30
+#define ZX297520V3_OUT2_WCLK 31
+#define ZX297520V3_OUT32K_WCLK 32
+#define ZX297520V3_RMIIPHY_WCLK 33
+#define ZX297520V3_TIMER_T12_WCLK 34
+#define ZX297520V3_TIMER_T12_PCLK 35
+#define ZX297520V3_TIMER_T13_WCLK 36
+#define ZX297520V3_TIMER_T13_PCLK 37
+#define ZX297520V3_TIMER_T14_WCLK 38
+#define ZX297520V3_TIMER_T14_PCLK 39
+#define ZX297520V3_TIMER_T15_WCLK 40
+#define ZX297520V3_TIMER_T15_PCLK 41
+#define ZX297520V3_TIMER_T16_WCLK 42
+#define ZX297520V3_TIMER_T16_PCLK 43
+#define ZX297520V3_TIMER_T17_WCLK 44
+#define ZX297520V3_TIMER_T17_PCLK 45
+#define ZX297520V3_WDT_T18_WCLK 46
+#define ZX297520V3_WDT_T18_PCLK 47
+#define ZX297520V3_USIM1_WCLK 48
+#define ZX297520V3_USIM1_PCLK 49
+#define ZX297520V3_AHB_WCLK 50
+#define ZX297520V3_AHB_PCLK 51
+#define ZX297520V3_USB_WCLK 52
+#define ZX297520V3_USB_PCLK 53
+#define ZX297520V3_HSIC_WCLK 54
+#define ZX297520V3_HSIC_PCLK 55
+
+#endif /* __DT_BINDINGS_CLOCK_ZX297520V3_H */
diff --git a/include/dt-bindings/phy/zte,zx297520v3-topcrm.h b/include/dt-bindings/phy/zte,zx297520v3-topcrm.h
new file mode 100644
index 000000000000..b36ccf2f54ef
--- /dev/null
+++ b/include/dt-bindings/phy/zte,zx297520v3-topcrm.h
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
+/*
+ * Copyright (C) Stefan Dösinger.
+ */
+
+#ifndef __DT_BINDINGS_PHY_ZTE_ZX297520V3_TOPCRM_H
+#define __DT_BINDINGS_PHY_ZTE_ZX297520V3_TOPCRM_H
+
+#define ZX297520V3_USB_PHY 0
+#define ZX297520V3_HSIC_PHY 1
+
+#endif /* __DT_BINDINGS_PHY_ZTE_ZX297520V3_TOPCRM_H */
diff --git a/include/dt-bindings/reset/zte,zx297520v3-reset.h b/include/dt-bindings/reset/zte,zx297520v3-reset.h
new file mode 100644
index 000000000000..43db72bb59de
--- /dev/null
+++ b/include/dt-bindings/reset/zte,zx297520v3-reset.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
+/*
+ * Copyright (C) Stefan Dösinger.
+ */
+
+#ifndef __DT_BINDINGS_RESET_ZX297520V3_H
+#define __DT_BINDINGS_RESET_ZX297520V3_H
+
+#define ZX297520V3_ZSP_RESET 0
+#define ZX297520V3_UART0_RESET 1
+#define ZX297520V3_I2C0_RESET 2
+#define ZX297520V3_RTC_RESET 3
+#define ZX297520V3_TIMER_T08_RESET 4
+#define ZX297520V3_TIMER_T09_RESET 5
+#define ZX297520V3_PMM_RESET 6
+#define ZX297520V3_GPIO_RESET 7
+#define ZX297520V3_GPIO8_RESET 8
+#define ZX297520V3_TIMER_T12_RESET 9
+#define ZX297520V3_TIMER_T13_RESET 10
+#define ZX297520V3_TIMER_T14_RESET 11
+#define ZX297520V3_TIMER_T15_RESET 12
+#define ZX297520V3_TIMER_T16_RESET 13
+#define ZX297520V3_TIMER_T17_RESET 14
+#define ZX297520V3_WDT_T18_RESET 15
+#define ZX297520V3_USIM1_RESET 16
+#define ZX297520V3_AHB_RESET 17
+#define ZX297520V3_USB_PHY_RESET 18
+#define ZX297520V3_USB_RESET 19
+#define ZX297520V3_HSIC_PHY_RESET 20
+#define ZX297520V3_HSIC_RESET 21
+
+#endif /* __DT_BINDINGS_RESET_ZX297520V3_H */
--
2.54.0
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 50+ messages in thread* Re: [PATCH v8 01/12] dt-bindings: clk: zte: Add zx297520v3 top clock and reset controller
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 18:32 ` sashiko-bot
-1 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 18:32 UTC (permalink / raw)
To: Stefan Dösinger
Cc: neil.armstrong, linux-phy, robh, mfd, vkoul, conor+dt, lee,
olteanv, devicetree
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Self-referencing cyclic dependency enforced by required `resets` property on the reset provider itself.
--
--- Patch [1]: [PATCH v8 01/12] dt-bindings: clk: zte: Add zx297520v3 top clock and reset controller ---
commit bd10e0d2ede4b7523e3aa201c9dc948ccee1e352
Author: Stefan Dösinger <stefandoesinger@gmail.com>
Subject: dt-bindings: clk: zte: Add zx297520v3 top clock and reset controller
This commit adds a new device tree binding for the ZTE zx297520v3 top clock
and reset controller. It describes the clock inputs, PLL outputs, and reset
controls for various peripherals. The binding also exposes USB and HSIC PHYs,
and a syscon-reboot method.
> diff --git a/Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml b/Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
> new file mode 100644
> index 0000000000000..9dbca49871a0e
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
[ ... ]
> + interrupt-names:
> + items:
> + - const: usb-up
> + - const: usb-down
> + - const: hsic-up
> + - const: hsic-down
> +
> + resets:
> + items:
> + - description: USB phy reset
> + - description: HSIC phy reset
> +
> + reset-names:
> + items:
> + - const: usb
> + - const: hsic
> +
> + "#clock-cells":
> + const: 1
> +
> + "#phy-cells":
> + const: 1
> +
> + "#reset-cells":
> + const: 1
> +
> + syscon-reboot:
> + type: object
> + $ref: /schemas/power/reset/syscon-reboot.yaml#
> + description:
> + Reboot method for the SoC.
> +
> +required:
> + - compatible
> + - reg
> + - clocks
> + - clock-names
> + - interrupts
> + - interrupt-names
> + - resets
> + - reset-names
[Severity: Medium]
Is it necessary to make the resets property required here?
Since this node is also the reset provider (indicated by #reset-cells),
requiring it to consume its own resets creates a self-referencing cyclic
dependency during device tree probing.
Could the driver interact with its own registers directly to handle these
resets, rather than acting as a reset consumer of itself?
> + - syscon-reboot
> + - "#clock-cells"
> + - "#phy-cells"
> + - "#reset-cells"
> +
> +unevaluatedProperties: false
> +
> +examples:
> + - |
> + #include <dt-bindings/interrupt-controller/arm-gic.h>
> + #include <dt-bindings/reset/zte,zx297520v3-reset.h>
> + #include <dt-bindings/phy/zte,zx297520v3-topcrm.h>
> + #include <dt-bindings/clock/zte,zx297520v3-clk.h>
> +
> + topcrm: clock-controller@13b000 {
> + compatible = "zte,zx297520v3-topcrm", "syscon";
> + reg = <0x0013b000 0x400>;
> + clocks = <&osc26m>, <&osc32k>;
> + clock-names = "osc26m", "osc32k";
> + interrupts = <GIC_SPI 42 IRQ_TYPE_EDGE_RISING>,
> + <GIC_SPI 43 IRQ_TYPE_EDGE_RISING>,
> + <GIC_SPI 45 IRQ_TYPE_EDGE_RISING>,
> + <GIC_SPI 46 IRQ_TYPE_EDGE_RISING>;
> + interrupt-names = "usb-up", "usb-down", "hsic-up", "hsic-down";
> + resets = <&topcrm ZX297520V3_USB_PHY_RESET>,
> + <&topcrm ZX297520V3_HSIC_PHY_RESET>;
[Severity: Medium]
Does modeling the internal register operations as external consumer
dependencies artificially create the dependency cycles mentioned in the commit
message?
The example explicitly demonstrates the self-referencing cyclic dependency
back to the topcrm phandle. Typically, it is an anti-pattern for a reset
provider to model its own internal signal lines as external dependencies
pointing back to itself.
If the driver handled its own internal reset logic directly, this cyclic
dependency could be avoided without relying on fw_devlink to forcefully break
it.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=1
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 50+ messages in thread* Re: [PATCH v8 01/12] dt-bindings: clk: zte: Add zx297520v3 top clock and reset controller
@ 2026-07-27 18:32 ` sashiko-bot
0 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 18:32 UTC (permalink / raw)
To: Stefan Dösinger
Cc: neil.armstrong, linux-phy, robh, mfd, vkoul, conor+dt, lee,
olteanv, devicetree
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Self-referencing cyclic dependency enforced by required `resets` property on the reset provider itself.
--
--- Patch [1]: [PATCH v8 01/12] dt-bindings: clk: zte: Add zx297520v3 top clock and reset controller ---
commit bd10e0d2ede4b7523e3aa201c9dc948ccee1e352
Author: Stefan Dösinger <stefandoesinger@gmail.com>
Subject: dt-bindings: clk: zte: Add zx297520v3 top clock and reset controller
This commit adds a new device tree binding for the ZTE zx297520v3 top clock
and reset controller. It describes the clock inputs, PLL outputs, and reset
controls for various peripherals. The binding also exposes USB and HSIC PHYs,
and a syscon-reboot method.
> diff --git a/Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml b/Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
> new file mode 100644
> index 0000000000000..9dbca49871a0e
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
[ ... ]
> + interrupt-names:
> + items:
> + - const: usb-up
> + - const: usb-down
> + - const: hsic-up
> + - const: hsic-down
> +
> + resets:
> + items:
> + - description: USB phy reset
> + - description: HSIC phy reset
> +
> + reset-names:
> + items:
> + - const: usb
> + - const: hsic
> +
> + "#clock-cells":
> + const: 1
> +
> + "#phy-cells":
> + const: 1
> +
> + "#reset-cells":
> + const: 1
> +
> + syscon-reboot:
> + type: object
> + $ref: /schemas/power/reset/syscon-reboot.yaml#
> + description:
> + Reboot method for the SoC.
> +
> +required:
> + - compatible
> + - reg
> + - clocks
> + - clock-names
> + - interrupts
> + - interrupt-names
> + - resets
> + - reset-names
[Severity: Medium]
Is it necessary to make the resets property required here?
Since this node is also the reset provider (indicated by #reset-cells),
requiring it to consume its own resets creates a self-referencing cyclic
dependency during device tree probing.
Could the driver interact with its own registers directly to handle these
resets, rather than acting as a reset consumer of itself?
> + - syscon-reboot
> + - "#clock-cells"
> + - "#phy-cells"
> + - "#reset-cells"
> +
> +unevaluatedProperties: false
> +
> +examples:
> + - |
> + #include <dt-bindings/interrupt-controller/arm-gic.h>
> + #include <dt-bindings/reset/zte,zx297520v3-reset.h>
> + #include <dt-bindings/phy/zte,zx297520v3-topcrm.h>
> + #include <dt-bindings/clock/zte,zx297520v3-clk.h>
> +
> + topcrm: clock-controller@13b000 {
> + compatible = "zte,zx297520v3-topcrm", "syscon";
> + reg = <0x0013b000 0x400>;
> + clocks = <&osc26m>, <&osc32k>;
> + clock-names = "osc26m", "osc32k";
> + interrupts = <GIC_SPI 42 IRQ_TYPE_EDGE_RISING>,
> + <GIC_SPI 43 IRQ_TYPE_EDGE_RISING>,
> + <GIC_SPI 45 IRQ_TYPE_EDGE_RISING>,
> + <GIC_SPI 46 IRQ_TYPE_EDGE_RISING>;
> + interrupt-names = "usb-up", "usb-down", "hsic-up", "hsic-down";
> + resets = <&topcrm ZX297520V3_USB_PHY_RESET>,
> + <&topcrm ZX297520V3_HSIC_PHY_RESET>;
[Severity: Medium]
Does modeling the internal register operations as external consumer
dependencies artificially create the dependency cycles mentioned in the commit
message?
The example explicitly demonstrates the self-referencing cyclic dependency
back to the topcrm phandle. Typically, it is an anti-pattern for a reset
provider to model its own internal signal lines as external dependencies
pointing back to itself.
If the driver handled its own internal reset logic directly, this cyclic
dependency could be avoided without relying on fw_devlink to forcefully break
it.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=1
^ permalink raw reply [flat|nested] 50+ messages in thread
* [PATCH v8 02/12] dt-bindings: clk: zte: Add zx297520v3 matrix clock and reset controller
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 18:24 ` Stefan Dösinger
-1 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
This controller contains clocks and resets for high speed devices on the
zx297520v3 board and hardware spinlocks that I expect will be necessary
to communicate correctly with the LTE DSP firmware blob.
I don't know much about the hwlocks yet other than that they exist, so
there is no header yet adding meaning to their indices. Presumably that
meaning doesn't exist in hardware and is de facto defined by ZTE's
firmware blobs running on the rprocs.
The zte-zx297520v3-crm driver will instantiate separate MFD children for
clocks and resets, which are handled by separate clock and reset
drivers.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
v7->v8:
*) Start clock indices at 0
*) Fix indentation
*) Add a prose note about discovered registers that allow writing
CNTVAL. I am not exposing them in any way because for now I see no need
to use this functionality. It is possible that the counter resets to 0
during standby, which means someone needs to fix it up using those
registers.
Nevertheless, no syscon yet for these reasons: I have no idea how CNTVAL
behaves during standby. It might be just fine. Standby very likely needs
firmware handling running on the Cortex M0, which can restore CNTVAL if
necessary. If Linux needs to do it, it can probably be done by the clock
or MFD parent driver. Given what I've learned so far I'd rather add
syscon later than put it in place without a clear user.
Details of how to manipulate CNTVAL will be in the matrix clk driver.
*) A non-change in the binding text, but worth mentioning: Register
0x118 contains gates that gate off matrix-synthesized PLL fixed
dividers. They reinforce the understanding of the top->matrix interface
and the adequacy of the bindings.
v6->v7:
*) Moved it back to clock/
*) Remove syscon compatible again. I looked into remaining unknown
registers and I am convinced that there is no odd misc register left
that can't be exposed as a proper subsystem provider.
The controls in 0x100 that ZTE's DRAM reading code exercises are auto
clock gating controls and the actual mailbox is in a different
controller.
Unknown registers in 0x120-0x12c are a frequency voting mechanism for
the AXI bus clock. The driver will participate in that voting rather
than set the frequency via 0x0. If need be it could be exposed
as #interconnect-cells, but it won't necessitate "syscon".
*) Reduce the number of input clocks to match register controls in
topclk's input control that affect matrix and LSP.
*) Add DDR mux and gates. Not to change them, but to make sure CCF knows
about which PLL is feeding the sysmem memory
Changes
v5->v6:
Add syscon compatible - Conor suggested this after v4 and I forgot it in v5.
v4->v5:
Move binding to soc/zte
Remove topclk from the example
Add #hwlock-cells for hw spinlock registers
Add more clocks I stumbled into: sram0 and another LTE related device
v3->v4:
Split matrixclk into its own controller again because syscon/regmap
deals poorly with device nodes that have more than one memory region. As
a consequence I am passing all PLL outputs generated on Topclk down to
Matrixclk.
---
.../bindings/clock/zte,zx297520v3-matrixcrm.yaml | 96 ++++++++++++++++++++++
MAINTAINERS | 1 +
include/dt-bindings/clock/zte,zx297520v3-clk.h | 41 +++++++++
include/dt-bindings/reset/zte,zx297520v3-reset.h | 12 +++
4 files changed, 150 insertions(+)
diff --git a/Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml b/Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
new file mode 100644
index 000000000000..d96f9f374447
--- /dev/null
+++ b/Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
@@ -0,0 +1,96 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/clock/zte,zx297520v3-matrixcrm.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE zx297520v3 SoC matrix clock and reset controller
+
+maintainers:
+ - Stefan Dösinger <stefandoesinger@gmail.com>
+
+description: |
+ This controller contains clock and reset controls for high speed devices on
+ the zx297520v3 board: The CPU, RAM, SDIO and Ethernet clocks and resets are
+ found here. This controller requires PLL-generated clocks from Topcrm as well
+ as the fixed 26 MHz and 32 kHz oscillators found on this board.
+
+ This controller also contains hardware mutex registers for synchronization
+ with different processors on this board.
+
+ An additional curiosity are registers that allow setting (but not reading) the
+ ARM architected timer's CNTVAL. This is not exposed in the binding because
+ there shouldn't be a need to access it.
+
+ All available clocks are defined as preprocessor macros in the
+ "include/dt-bindings/clock/zte,zx297520v3-clk.h" header. Resets are defined in
+ the "include/dt-bindings/reset/zte,zx297520v3-reset.h" header.
+
+properties:
+ compatible:
+ const: zte,zx297520v3-matrixcrm
+
+ reg:
+ maxItems: 1
+
+ clocks:
+ items:
+ - description: 26 MHz external oscillator
+ - description: 32 kHz external oscillator
+ - description: Main PLL output from topcrm (usually 624 MHz)
+ - description: Main PLL subdivision factor 5
+ - description: Dpll output from topcrm (usually 491.52 MHz)
+ - description: Gpll output from topcrm (usually 200 MHz)
+ - description: Gpll subdivision factor 2
+ - description: osc26m work clock from topcrm
+
+ clock-names:
+ items:
+ - const: osc26m
+ - const: osc32k
+ - const: mpll
+ - const: mpll-d5
+ - const: dpll
+ - const: gpll
+ - const: gpll-d2
+ - const: wclk-osc26m
+
+ "#clock-cells":
+ const: 1
+
+ "#hwlock-cells":
+ const: 1
+
+ "#reset-cells":
+ const: 1
+
+required:
+ - compatible
+ - reg
+ - clocks
+ - clock-names
+ - "#clock-cells"
+ - "#hwlock-cells"
+ - "#reset-cells"
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/clock/zte,zx297520v3-clk.h>
+ #include <dt-bindings/reset/zte,zx297520v3-reset.h>
+
+ clock-controller@1306000 {
+ compatible = "zte,zx297520v3-matrixcrm";
+ reg = <0x01306000 0x400>;
+ clocks = <&osc26m>, <&osc32k>,
+ <&topcrm ZX297520V3_MPLL>, <&topcrm ZX297520V3_MPLL_D5>,
+ <&topcrm ZX297520V3_DPLL>, <&topcrm ZX297520V3_GPLL>,
+ <&topcrm ZX297520V3_GPLL_D2>,
+ <&topcrm ZX297520V3_GATED_OSC26M>;
+ clock-names = "osc26m", "osc32k", "mpll", "mpll-d5", "dpll", "gpll",
+ "gpll-d2", "wclk-osc26m";
+ #clock-cells = <1>;
+ #hwlock-cells = <1>;
+ #reset-cells = <1>;
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index 36619a9ff8fc..dc2972ade70f 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3878,6 +3878,7 @@ L: linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
S: Odd fixes
F: Documentation/arch/arm/zte/
F: Documentation/devicetree/bindings/arm/zte.yaml
+F: Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
F: Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
F: arch/arm/boot/dts/zte/
F: arch/arm/mach-zte/
diff --git a/include/dt-bindings/clock/zte,zx297520v3-clk.h b/include/dt-bindings/clock/zte,zx297520v3-clk.h
index 3aed94ccc26f..a8477e9b2a14 100644
--- a/include/dt-bindings/clock/zte,zx297520v3-clk.h
+++ b/include/dt-bindings/clock/zte,zx297520v3-clk.h
@@ -63,4 +63,45 @@
#define ZX297520V3_HSIC_WCLK 54
#define ZX297520V3_HSIC_PCLK 55
+#define ZX297520V3_SYS_TIMER_WCLK 0
+#define ZX297520V3_CPU_WCLK 1
+#define ZX297520V3_CPU_PCLK 2
+#define ZX297520V3_ZSP_WCLK 3
+#define ZX297520V3_DDR_CTRL_PCLK 4
+#define ZX297520V3_DDR_CTRL_WCLK 5
+#define ZX297520V3_EDCP_WCLK 6
+#define ZX297520V3_EDCP_PCLK 7
+#define ZX297520V3_SD0_WCLK 8
+#define ZX297520V3_SD0_PCLK 9
+#define ZX297520V3_SD0_CDET 10
+#define ZX297520V3_SD1_WCLK 11
+#define ZX297520V3_SD1_PCLK 12
+#define ZX297520V3_SD1_CDET 13
+#define ZX297520V3_NAND_WCLK 14
+#define ZX297520V3_NAND_PCLK 15
+#define ZX297520V3_DMA_PCLK 16
+#define ZX297520V3_MBOX_PCLK 17
+#define ZX297520V3_PDCFG_WCLK 18
+#define ZX297520V3_PDCFG_PCLK 19
+#define ZX297520V3_SSC_WCLK 20
+#define ZX297520V3_SSC_PCLK 21
+#define ZX297520V3_AXI_WCLK 22
+#define ZX297520V3_GMAC_WCLK 23
+#define ZX297520V3_GMAC_PCLK 24
+#define ZX297520V3_GMAC_AHB 25
+#define ZX297520V3_VOU_WCLK 26
+#define ZX297520V3_VOU_PCLK 27
+#define ZX297520V3_LSP_MPLL_D5_WCLK 28
+#define ZX297520V3_LSP_MPLL_D4_WCLK 29
+#define ZX297520V3_LSP_MPLL_D6_WCLK 30
+#define ZX297520V3_LSP_MPLL_D8_WCLK 31
+#define ZX297520V3_LSP_MPLL_D12_WCLK 32
+#define ZX297520V3_LSP_OSC26M_WCLK 33
+#define ZX297520V3_LSP_OSC32K_WCLK 34
+#define ZX297520V3_LSP_PCLK 35
+#define ZX297520V3_LSP_TDM_WCLK 36
+#define ZX297520V3_LSP_DPLL_D4_WCLK 37
+#define ZX297520V3_SRAM0_PCLK 38
+#define ZX297520V3_GSM_CFG_PCLK 39
+
#endif /* __DT_BINDINGS_CLOCK_ZX297520V3_H */
diff --git a/include/dt-bindings/reset/zte,zx297520v3-reset.h b/include/dt-bindings/reset/zte,zx297520v3-reset.h
index 43db72bb59de..4682d4a599c6 100644
--- a/include/dt-bindings/reset/zte,zx297520v3-reset.h
+++ b/include/dt-bindings/reset/zte,zx297520v3-reset.h
@@ -29,4 +29,16 @@
#define ZX297520V3_HSIC_PHY_RESET 20
#define ZX297520V3_HSIC_RESET 21
+#define ZX297520V3_CPU_RESET 0
+#define ZX297520V3_DDR_CTRL_RESET 1
+#define ZX297520V3_EDCP_RESET 2
+#define ZX297520V3_SD0_RESET 3
+#define ZX297520V3_SD1_RESET 4
+#define ZX297520V3_NAND_RESET 5
+#define ZX297520V3_PDCFG_RESET 6
+#define ZX297520V3_SSC_RESET 7
+#define ZX297520V3_GMAC_RESET 8
+#define ZX297520V3_VOU_RESET 9
+#define ZX297520V3_LSP_RESET 10
+
#endif /* __DT_BINDINGS_RESET_ZX297520V3_H */
--
2.54.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* [PATCH v8 02/12] dt-bindings: clk: zte: Add zx297520v3 matrix clock and reset controller
@ 2026-07-27 18:24 ` Stefan Dösinger
0 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
This controller contains clocks and resets for high speed devices on the
zx297520v3 board and hardware spinlocks that I expect will be necessary
to communicate correctly with the LTE DSP firmware blob.
I don't know much about the hwlocks yet other than that they exist, so
there is no header yet adding meaning to their indices. Presumably that
meaning doesn't exist in hardware and is de facto defined by ZTE's
firmware blobs running on the rprocs.
The zte-zx297520v3-crm driver will instantiate separate MFD children for
clocks and resets, which are handled by separate clock and reset
drivers.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
v7->v8:
*) Start clock indices at 0
*) Fix indentation
*) Add a prose note about discovered registers that allow writing
CNTVAL. I am not exposing them in any way because for now I see no need
to use this functionality. It is possible that the counter resets to 0
during standby, which means someone needs to fix it up using those
registers.
Nevertheless, no syscon yet for these reasons: I have no idea how CNTVAL
behaves during standby. It might be just fine. Standby very likely needs
firmware handling running on the Cortex M0, which can restore CNTVAL if
necessary. If Linux needs to do it, it can probably be done by the clock
or MFD parent driver. Given what I've learned so far I'd rather add
syscon later than put it in place without a clear user.
Details of how to manipulate CNTVAL will be in the matrix clk driver.
*) A non-change in the binding text, but worth mentioning: Register
0x118 contains gates that gate off matrix-synthesized PLL fixed
dividers. They reinforce the understanding of the top->matrix interface
and the adequacy of the bindings.
v6->v7:
*) Moved it back to clock/
*) Remove syscon compatible again. I looked into remaining unknown
registers and I am convinced that there is no odd misc register left
that can't be exposed as a proper subsystem provider.
The controls in 0x100 that ZTE's DRAM reading code exercises are auto
clock gating controls and the actual mailbox is in a different
controller.
Unknown registers in 0x120-0x12c are a frequency voting mechanism for
the AXI bus clock. The driver will participate in that voting rather
than set the frequency via 0x0. If need be it could be exposed
as #interconnect-cells, but it won't necessitate "syscon".
*) Reduce the number of input clocks to match register controls in
topclk's input control that affect matrix and LSP.
*) Add DDR mux and gates. Not to change them, but to make sure CCF knows
about which PLL is feeding the sysmem memory
Changes
v5->v6:
Add syscon compatible - Conor suggested this after v4 and I forgot it in v5.
v4->v5:
Move binding to soc/zte
Remove topclk from the example
Add #hwlock-cells for hw spinlock registers
Add more clocks I stumbled into: sram0 and another LTE related device
v3->v4:
Split matrixclk into its own controller again because syscon/regmap
deals poorly with device nodes that have more than one memory region. As
a consequence I am passing all PLL outputs generated on Topclk down to
Matrixclk.
---
.../bindings/clock/zte,zx297520v3-matrixcrm.yaml | 96 ++++++++++++++++++++++
MAINTAINERS | 1 +
include/dt-bindings/clock/zte,zx297520v3-clk.h | 41 +++++++++
include/dt-bindings/reset/zte,zx297520v3-reset.h | 12 +++
4 files changed, 150 insertions(+)
diff --git a/Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml b/Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
new file mode 100644
index 000000000000..d96f9f374447
--- /dev/null
+++ b/Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
@@ -0,0 +1,96 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/clock/zte,zx297520v3-matrixcrm.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE zx297520v3 SoC matrix clock and reset controller
+
+maintainers:
+ - Stefan Dösinger <stefandoesinger@gmail.com>
+
+description: |
+ This controller contains clock and reset controls for high speed devices on
+ the zx297520v3 board: The CPU, RAM, SDIO and Ethernet clocks and resets are
+ found here. This controller requires PLL-generated clocks from Topcrm as well
+ as the fixed 26 MHz and 32 kHz oscillators found on this board.
+
+ This controller also contains hardware mutex registers for synchronization
+ with different processors on this board.
+
+ An additional curiosity are registers that allow setting (but not reading) the
+ ARM architected timer's CNTVAL. This is not exposed in the binding because
+ there shouldn't be a need to access it.
+
+ All available clocks are defined as preprocessor macros in the
+ "include/dt-bindings/clock/zte,zx297520v3-clk.h" header. Resets are defined in
+ the "include/dt-bindings/reset/zte,zx297520v3-reset.h" header.
+
+properties:
+ compatible:
+ const: zte,zx297520v3-matrixcrm
+
+ reg:
+ maxItems: 1
+
+ clocks:
+ items:
+ - description: 26 MHz external oscillator
+ - description: 32 kHz external oscillator
+ - description: Main PLL output from topcrm (usually 624 MHz)
+ - description: Main PLL subdivision factor 5
+ - description: Dpll output from topcrm (usually 491.52 MHz)
+ - description: Gpll output from topcrm (usually 200 MHz)
+ - description: Gpll subdivision factor 2
+ - description: osc26m work clock from topcrm
+
+ clock-names:
+ items:
+ - const: osc26m
+ - const: osc32k
+ - const: mpll
+ - const: mpll-d5
+ - const: dpll
+ - const: gpll
+ - const: gpll-d2
+ - const: wclk-osc26m
+
+ "#clock-cells":
+ const: 1
+
+ "#hwlock-cells":
+ const: 1
+
+ "#reset-cells":
+ const: 1
+
+required:
+ - compatible
+ - reg
+ - clocks
+ - clock-names
+ - "#clock-cells"
+ - "#hwlock-cells"
+ - "#reset-cells"
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/clock/zte,zx297520v3-clk.h>
+ #include <dt-bindings/reset/zte,zx297520v3-reset.h>
+
+ clock-controller@1306000 {
+ compatible = "zte,zx297520v3-matrixcrm";
+ reg = <0x01306000 0x400>;
+ clocks = <&osc26m>, <&osc32k>,
+ <&topcrm ZX297520V3_MPLL>, <&topcrm ZX297520V3_MPLL_D5>,
+ <&topcrm ZX297520V3_DPLL>, <&topcrm ZX297520V3_GPLL>,
+ <&topcrm ZX297520V3_GPLL_D2>,
+ <&topcrm ZX297520V3_GATED_OSC26M>;
+ clock-names = "osc26m", "osc32k", "mpll", "mpll-d5", "dpll", "gpll",
+ "gpll-d2", "wclk-osc26m";
+ #clock-cells = <1>;
+ #hwlock-cells = <1>;
+ #reset-cells = <1>;
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index 36619a9ff8fc..dc2972ade70f 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3878,6 +3878,7 @@ L: linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
S: Odd fixes
F: Documentation/arch/arm/zte/
F: Documentation/devicetree/bindings/arm/zte.yaml
+F: Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
F: Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
F: arch/arm/boot/dts/zte/
F: arch/arm/mach-zte/
diff --git a/include/dt-bindings/clock/zte,zx297520v3-clk.h b/include/dt-bindings/clock/zte,zx297520v3-clk.h
index 3aed94ccc26f..a8477e9b2a14 100644
--- a/include/dt-bindings/clock/zte,zx297520v3-clk.h
+++ b/include/dt-bindings/clock/zte,zx297520v3-clk.h
@@ -63,4 +63,45 @@
#define ZX297520V3_HSIC_WCLK 54
#define ZX297520V3_HSIC_PCLK 55
+#define ZX297520V3_SYS_TIMER_WCLK 0
+#define ZX297520V3_CPU_WCLK 1
+#define ZX297520V3_CPU_PCLK 2
+#define ZX297520V3_ZSP_WCLK 3
+#define ZX297520V3_DDR_CTRL_PCLK 4
+#define ZX297520V3_DDR_CTRL_WCLK 5
+#define ZX297520V3_EDCP_WCLK 6
+#define ZX297520V3_EDCP_PCLK 7
+#define ZX297520V3_SD0_WCLK 8
+#define ZX297520V3_SD0_PCLK 9
+#define ZX297520V3_SD0_CDET 10
+#define ZX297520V3_SD1_WCLK 11
+#define ZX297520V3_SD1_PCLK 12
+#define ZX297520V3_SD1_CDET 13
+#define ZX297520V3_NAND_WCLK 14
+#define ZX297520V3_NAND_PCLK 15
+#define ZX297520V3_DMA_PCLK 16
+#define ZX297520V3_MBOX_PCLK 17
+#define ZX297520V3_PDCFG_WCLK 18
+#define ZX297520V3_PDCFG_PCLK 19
+#define ZX297520V3_SSC_WCLK 20
+#define ZX297520V3_SSC_PCLK 21
+#define ZX297520V3_AXI_WCLK 22
+#define ZX297520V3_GMAC_WCLK 23
+#define ZX297520V3_GMAC_PCLK 24
+#define ZX297520V3_GMAC_AHB 25
+#define ZX297520V3_VOU_WCLK 26
+#define ZX297520V3_VOU_PCLK 27
+#define ZX297520V3_LSP_MPLL_D5_WCLK 28
+#define ZX297520V3_LSP_MPLL_D4_WCLK 29
+#define ZX297520V3_LSP_MPLL_D6_WCLK 30
+#define ZX297520V3_LSP_MPLL_D8_WCLK 31
+#define ZX297520V3_LSP_MPLL_D12_WCLK 32
+#define ZX297520V3_LSP_OSC26M_WCLK 33
+#define ZX297520V3_LSP_OSC32K_WCLK 34
+#define ZX297520V3_LSP_PCLK 35
+#define ZX297520V3_LSP_TDM_WCLK 36
+#define ZX297520V3_LSP_DPLL_D4_WCLK 37
+#define ZX297520V3_SRAM0_PCLK 38
+#define ZX297520V3_GSM_CFG_PCLK 39
+
#endif /* __DT_BINDINGS_CLOCK_ZX297520V3_H */
diff --git a/include/dt-bindings/reset/zte,zx297520v3-reset.h b/include/dt-bindings/reset/zte,zx297520v3-reset.h
index 43db72bb59de..4682d4a599c6 100644
--- a/include/dt-bindings/reset/zte,zx297520v3-reset.h
+++ b/include/dt-bindings/reset/zte,zx297520v3-reset.h
@@ -29,4 +29,16 @@
#define ZX297520V3_HSIC_PHY_RESET 20
#define ZX297520V3_HSIC_RESET 21
+#define ZX297520V3_CPU_RESET 0
+#define ZX297520V3_DDR_CTRL_RESET 1
+#define ZX297520V3_EDCP_RESET 2
+#define ZX297520V3_SD0_RESET 3
+#define ZX297520V3_SD1_RESET 4
+#define ZX297520V3_NAND_RESET 5
+#define ZX297520V3_PDCFG_RESET 6
+#define ZX297520V3_SSC_RESET 7
+#define ZX297520V3_GMAC_RESET 8
+#define ZX297520V3_VOU_RESET 9
+#define ZX297520V3_LSP_RESET 10
+
#endif /* __DT_BINDINGS_RESET_ZX297520V3_H */
--
2.54.0
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 50+ messages in thread* Re: [PATCH v8 02/12] dt-bindings: clk: zte: Add zx297520v3 matrix clock and reset controller
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 18:34 ` sashiko-bot
-1 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 18:34 UTC (permalink / raw)
To: Stefan Dösinger
Cc: vkoul, mfd, conor+dt, robh, neil.armstrong, linux-phy, devicetree,
lee, olteanv
> This controller contains clocks and resets for high speed devices on the
> zx297520v3 board and hardware spinlocks that I expect will be necessary
> to communicate correctly with the LTE DSP firmware blob.
>
> I don't know much about the hwlocks yet other than that they exist, so
> there is no header yet adding meaning to their indices. Presumably that
> meaning doesn't exist in hardware and is de facto defined by ZTE's
> firmware blobs running on the rprocs.
>
> The zte-zx297520v3-crm driver will instantiate separate MFD children for
> clocks and resets, which are handled by separate clock and reset
> drivers.
>
> Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=2
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 50+ messages in thread
* Re: [PATCH v8 02/12] dt-bindings: clk: zte: Add zx297520v3 matrix clock and reset controller
@ 2026-07-27 18:34 ` sashiko-bot
0 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 18:34 UTC (permalink / raw)
To: Stefan Dösinger
Cc: vkoul, mfd, conor+dt, robh, neil.armstrong, linux-phy, devicetree,
lee, olteanv
> This controller contains clocks and resets for high speed devices on the
> zx297520v3 board and hardware spinlocks that I expect will be necessary
> to communicate correctly with the LTE DSP firmware blob.
>
> I don't know much about the hwlocks yet other than that they exist, so
> there is no header yet adding meaning to their indices. Presumably that
> meaning doesn't exist in hardware and is de facto defined by ZTE's
> firmware blobs running on the rprocs.
>
> The zte-zx297520v3-crm driver will instantiate separate MFD children for
> clocks and resets, which are handled by separate clock and reset
> drivers.
>
> Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=2
^ permalink raw reply [flat|nested] 50+ messages in thread
* [PATCH v8 03/12] dt-bindings: clk: zte: Add zx297520v3 LSP clock and reset controller
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 18:24 ` Stefan Dösinger
-1 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger, Krzysztof Kozlowski
The clock controller of the zx297520v3 Low Speed Peripherals is
relatively clean. One register per device with gates, muxes and resets
and for some devices a divider. There are even bits in the matrix
controller to control propagation of clock lines down to LSP.
The clocks are sorted by register address and I am convinced that the
device list is complete. There are, however, a few more registers that
are likely extra dividers for TDM and I2S devices.
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
Patch changelog:
v8:
*) Start clock indices at 0
*) A minor grammar fix in the second paragraph of the binding prose
v5:
Rename from lspclk to lspcrm
Remove matrixcrm from example
v4: Order properties compatible->reg->clocks->clock->names->#cells
---
.../bindings/clock/zte,zx297520v3-lspcrm.yaml | 101 +++++++++++++++++++++
MAINTAINERS | 1 +
include/dt-bindings/clock/zte,zx297520v3-clk.h | 37 ++++++++
include/dt-bindings/reset/zte,zx297520v3-reset.h | 19 ++++
4 files changed, 158 insertions(+)
diff --git a/Documentation/devicetree/bindings/clock/zte,zx297520v3-lspcrm.yaml b/Documentation/devicetree/bindings/clock/zte,zx297520v3-lspcrm.yaml
new file mode 100644
index 000000000000..cfad0e0372f4
--- /dev/null
+++ b/Documentation/devicetree/bindings/clock/zte,zx297520v3-lspcrm.yaml
@@ -0,0 +1,101 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/clock/zte,zx297520v3-lspcrm.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE zx297520v3 SoC LSP clock and reset controller
+
+maintainers:
+ - Stefan Dösinger <stefandoesinger@gmail.com>
+
+description: |
+ This clock and reset controller controls low speed peripherals on the board.
+ This is a relatively isolated subsystem containing UART, I2C, I2S and SPI
+ devices. The controller is responsible for bringing the devices out of reset
+ and enabling their clocks as needed.
+
+ The controller receives its clock signals from the matrix controller, and
+ these signals need to be declared as clock inputs.
+
+ All available clocks are defined as preprocessor macros in the
+ "include/dt-bindings/clock/zte,zx297520v3-clk.h" header. Resets are defined in
+ the "include/dt-bindings/reset/zte,zx297520v3-reset.h" header.
+
+properties:
+ compatible:
+ const: zte,zx297520v3-lspcrm
+
+ reg:
+ maxItems: 1
+
+ clocks:
+ items:
+ - description: Main PLL divided by 5 output from matrixcrm (124.8 MHz)
+ - description: Main PLL divided by 4 output from matrixcrm (156 MHz)
+ - description: Main PLL divided by 6 output from matrixcrm (104 MHz)
+ - description: Main PLL divided by 8 output from matrixcrm (78 MHz)
+ - description: Main PLL divided by 12 output from matrixcrm (52 MHz)
+ - description: Main oscillator output from matrixcrm (26 MHz)
+ - description: Timer oscillator output from matrixcrm (32 kHz)
+ - description: LSP pclk output from matrixcrm (26 MHz)
+ - description: TDM wclk mux output from matrixcrm
+ - description: DPLL divided by 4 output from matrixcrm (122.88 MHz)
+
+ clock-names:
+ items:
+ - const: mpll-d5
+ - const: mpll-d4
+ - const: mpll-d6
+ - const: mpll-d8
+ - const: mpll-d12
+ - const: osc26m
+ - const: osc32k
+ - const: pclk
+ - const: tdm-wclk
+ - const: dpll-d4
+
+ resets:
+ maxItems: 1
+
+ "#clock-cells":
+ const: 1
+
+ "#reset-cells":
+ const: 1
+
+required:
+ - compatible
+ - reg
+ - clocks
+ - clock-names
+ - resets
+ - "#clock-cells"
+ - "#reset-cells"
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/clock/zte,zx297520v3-clk.h>
+ #include <dt-bindings/reset/zte,zx297520v3-reset.h>
+
+ clock-controller@1400000 {
+ compatible = "zte,zx297520v3-lspcrm";
+ reg = <0x01400000 0x100>;
+ clocks = <&matrixcrm ZX297520V3_LSP_MPLL_D5_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_MPLL_D4_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_MPLL_D6_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_MPLL_D8_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_MPLL_D12_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_OSC26M_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_OSC32K_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_PCLK>,
+ <&matrixcrm ZX297520V3_LSP_TDM_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_DPLL_D4_WCLK>;
+ clock-names = "mpll-d5", "mpll-d4", "mpll-d6", "mpll-d8", "mpll-d12",
+ "osc26m", "osc32k", "pclk", "tdm-wclk", "dpll-d4";
+ resets = <&matrixcrm ZX297520V3_LSP_RESET>;
+ #clock-cells = <1>;
+ #reset-cells = <1>;
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index dc2972ade70f..05322672b9b5 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3878,6 +3878,7 @@ L: linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
S: Odd fixes
F: Documentation/arch/arm/zte/
F: Documentation/devicetree/bindings/arm/zte.yaml
+F: Documentation/devicetree/bindings/clock/zte,zx297520v3-lspcrm.yaml
F: Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
F: Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
F: arch/arm/boot/dts/zte/
diff --git a/include/dt-bindings/clock/zte,zx297520v3-clk.h b/include/dt-bindings/clock/zte,zx297520v3-clk.h
index a8477e9b2a14..8824740468a3 100644
--- a/include/dt-bindings/clock/zte,zx297520v3-clk.h
+++ b/include/dt-bindings/clock/zte,zx297520v3-clk.h
@@ -104,4 +104,41 @@
#define ZX297520V3_SRAM0_PCLK 38
#define ZX297520V3_GSM_CFG_PCLK 39
+#define ZX297520V3_TIMER_L1_WCLK 0
+#define ZX297520V3_TIMER_L1_PCLK 1
+#define ZX297520V3_WDT_L2_WCLK 2
+#define ZX297520V3_WDT_L2_PCLK 3
+#define ZX297520V3_WDT_L3_WCLK 4
+#define ZX297520V3_WDT_L3_PCLK 5
+#define ZX297520V3_PWM_WCLK 6
+#define ZX297520V3_PWM_PCLK 7
+#define ZX297520V3_I2S0_WCLK 8
+#define ZX297520V3_I2S0_PCLK 9
+#define ZX297520V3_I2S1_WCLK 10
+#define ZX297520V3_I2S1_PCLK 11
+#define ZX297520V3_QSPI_WCLK 12
+#define ZX297520V3_QSPI_PCLK 13
+#define ZX297520V3_UART1_WCLK 14
+#define ZX297520V3_UART1_PCLK 15
+#define ZX297520V3_I2C1_WCLK 16
+#define ZX297520V3_I2C1_PCLK 17
+#define ZX297520V3_SPI0_WCLK 18
+#define ZX297520V3_SPI0_PCLK 19
+#define ZX297520V3_TIMER_LB_WCLK 20
+#define ZX297520V3_TIMER_LB_PCLK 21
+#define ZX297520V3_TIMER_LC_WCLK 22
+#define ZX297520V3_TIMER_LC_PCLK 23
+#define ZX297520V3_UART2_WCLK 24
+#define ZX297520V3_UART2_PCLK 25
+#define ZX297520V3_WDT_LE_WCLK 26
+#define ZX297520V3_WDT_LE_PCLK 27
+#define ZX297520V3_TIMER_LF_WCLK 28
+#define ZX297520V3_TIMER_LF_PCLK 29
+#define ZX297520V3_SPI1_WCLK 30
+#define ZX297520V3_SPI1_PCLK 31
+#define ZX297520V3_TIMER_L11_WCLK 32
+#define ZX297520V3_TIMER_L11_PCLK 33
+#define ZX297520V3_TDM_WCLK 34
+#define ZX297520V3_TDM_PCLK 35
+
#endif /* __DT_BINDINGS_CLOCK_ZX297520V3_H */
diff --git a/include/dt-bindings/reset/zte,zx297520v3-reset.h b/include/dt-bindings/reset/zte,zx297520v3-reset.h
index 4682d4a599c6..69eef84b2f29 100644
--- a/include/dt-bindings/reset/zte,zx297520v3-reset.h
+++ b/include/dt-bindings/reset/zte,zx297520v3-reset.h
@@ -41,4 +41,23 @@
#define ZX297520V3_VOU_RESET 9
#define ZX297520V3_LSP_RESET 10
+#define ZX297520V3_TIMER_L1_RESET 0
+#define ZX297520V3_WDT_L2_RESET 1
+#define ZX297520V3_WDT_L3_RESET 2
+#define ZX297520V3_PWM_RESET 3
+#define ZX297520V3_I2S0_RESET 4
+#define ZX297520V3_I2S1_RESET 5
+#define ZX297520V3_QSPI_RESET 6
+#define ZX297520V3_UART1_RESET 7
+#define ZX297520V3_I2C1_RESET 8
+#define ZX297520V3_SPI0_RESET 9
+#define ZX297520V3_TIMER_LB_RESET 10
+#define ZX297520V3_TIMER_LC_RESET 11
+#define ZX297520V3_UART2_RESET 12
+#define ZX297520V3_WDT_LE_RESET 13
+#define ZX297520V3_TIMER_LF_RESET 14
+#define ZX297520V3_SPI1_RESET 15
+#define ZX297520V3_TIMER_L11_RESET 16
+#define ZX297520V3_TDM_RESET 17
+
#endif /* __DT_BINDINGS_RESET_ZX297520V3_H */
--
2.54.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* [PATCH v8 03/12] dt-bindings: clk: zte: Add zx297520v3 LSP clock and reset controller
@ 2026-07-27 18:24 ` Stefan Dösinger
0 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger, Krzysztof Kozlowski
The clock controller of the zx297520v3 Low Speed Peripherals is
relatively clean. One register per device with gates, muxes and resets
and for some devices a divider. There are even bits in the matrix
controller to control propagation of clock lines down to LSP.
The clocks are sorted by register address and I am convinced that the
device list is complete. There are, however, a few more registers that
are likely extra dividers for TDM and I2S devices.
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
Patch changelog:
v8:
*) Start clock indices at 0
*) A minor grammar fix in the second paragraph of the binding prose
v5:
Rename from lspclk to lspcrm
Remove matrixcrm from example
v4: Order properties compatible->reg->clocks->clock->names->#cells
---
.../bindings/clock/zte,zx297520v3-lspcrm.yaml | 101 +++++++++++++++++++++
MAINTAINERS | 1 +
include/dt-bindings/clock/zte,zx297520v3-clk.h | 37 ++++++++
include/dt-bindings/reset/zte,zx297520v3-reset.h | 19 ++++
4 files changed, 158 insertions(+)
diff --git a/Documentation/devicetree/bindings/clock/zte,zx297520v3-lspcrm.yaml b/Documentation/devicetree/bindings/clock/zte,zx297520v3-lspcrm.yaml
new file mode 100644
index 000000000000..cfad0e0372f4
--- /dev/null
+++ b/Documentation/devicetree/bindings/clock/zte,zx297520v3-lspcrm.yaml
@@ -0,0 +1,101 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/clock/zte,zx297520v3-lspcrm.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE zx297520v3 SoC LSP clock and reset controller
+
+maintainers:
+ - Stefan Dösinger <stefandoesinger@gmail.com>
+
+description: |
+ This clock and reset controller controls low speed peripherals on the board.
+ This is a relatively isolated subsystem containing UART, I2C, I2S and SPI
+ devices. The controller is responsible for bringing the devices out of reset
+ and enabling their clocks as needed.
+
+ The controller receives its clock signals from the matrix controller, and
+ these signals need to be declared as clock inputs.
+
+ All available clocks are defined as preprocessor macros in the
+ "include/dt-bindings/clock/zte,zx297520v3-clk.h" header. Resets are defined in
+ the "include/dt-bindings/reset/zte,zx297520v3-reset.h" header.
+
+properties:
+ compatible:
+ const: zte,zx297520v3-lspcrm
+
+ reg:
+ maxItems: 1
+
+ clocks:
+ items:
+ - description: Main PLL divided by 5 output from matrixcrm (124.8 MHz)
+ - description: Main PLL divided by 4 output from matrixcrm (156 MHz)
+ - description: Main PLL divided by 6 output from matrixcrm (104 MHz)
+ - description: Main PLL divided by 8 output from matrixcrm (78 MHz)
+ - description: Main PLL divided by 12 output from matrixcrm (52 MHz)
+ - description: Main oscillator output from matrixcrm (26 MHz)
+ - description: Timer oscillator output from matrixcrm (32 kHz)
+ - description: LSP pclk output from matrixcrm (26 MHz)
+ - description: TDM wclk mux output from matrixcrm
+ - description: DPLL divided by 4 output from matrixcrm (122.88 MHz)
+
+ clock-names:
+ items:
+ - const: mpll-d5
+ - const: mpll-d4
+ - const: mpll-d6
+ - const: mpll-d8
+ - const: mpll-d12
+ - const: osc26m
+ - const: osc32k
+ - const: pclk
+ - const: tdm-wclk
+ - const: dpll-d4
+
+ resets:
+ maxItems: 1
+
+ "#clock-cells":
+ const: 1
+
+ "#reset-cells":
+ const: 1
+
+required:
+ - compatible
+ - reg
+ - clocks
+ - clock-names
+ - resets
+ - "#clock-cells"
+ - "#reset-cells"
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/clock/zte,zx297520v3-clk.h>
+ #include <dt-bindings/reset/zte,zx297520v3-reset.h>
+
+ clock-controller@1400000 {
+ compatible = "zte,zx297520v3-lspcrm";
+ reg = <0x01400000 0x100>;
+ clocks = <&matrixcrm ZX297520V3_LSP_MPLL_D5_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_MPLL_D4_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_MPLL_D6_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_MPLL_D8_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_MPLL_D12_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_OSC26M_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_OSC32K_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_PCLK>,
+ <&matrixcrm ZX297520V3_LSP_TDM_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_DPLL_D4_WCLK>;
+ clock-names = "mpll-d5", "mpll-d4", "mpll-d6", "mpll-d8", "mpll-d12",
+ "osc26m", "osc32k", "pclk", "tdm-wclk", "dpll-d4";
+ resets = <&matrixcrm ZX297520V3_LSP_RESET>;
+ #clock-cells = <1>;
+ #reset-cells = <1>;
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index dc2972ade70f..05322672b9b5 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3878,6 +3878,7 @@ L: linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
S: Odd fixes
F: Documentation/arch/arm/zte/
F: Documentation/devicetree/bindings/arm/zte.yaml
+F: Documentation/devicetree/bindings/clock/zte,zx297520v3-lspcrm.yaml
F: Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
F: Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
F: arch/arm/boot/dts/zte/
diff --git a/include/dt-bindings/clock/zte,zx297520v3-clk.h b/include/dt-bindings/clock/zte,zx297520v3-clk.h
index a8477e9b2a14..8824740468a3 100644
--- a/include/dt-bindings/clock/zte,zx297520v3-clk.h
+++ b/include/dt-bindings/clock/zte,zx297520v3-clk.h
@@ -104,4 +104,41 @@
#define ZX297520V3_SRAM0_PCLK 38
#define ZX297520V3_GSM_CFG_PCLK 39
+#define ZX297520V3_TIMER_L1_WCLK 0
+#define ZX297520V3_TIMER_L1_PCLK 1
+#define ZX297520V3_WDT_L2_WCLK 2
+#define ZX297520V3_WDT_L2_PCLK 3
+#define ZX297520V3_WDT_L3_WCLK 4
+#define ZX297520V3_WDT_L3_PCLK 5
+#define ZX297520V3_PWM_WCLK 6
+#define ZX297520V3_PWM_PCLK 7
+#define ZX297520V3_I2S0_WCLK 8
+#define ZX297520V3_I2S0_PCLK 9
+#define ZX297520V3_I2S1_WCLK 10
+#define ZX297520V3_I2S1_PCLK 11
+#define ZX297520V3_QSPI_WCLK 12
+#define ZX297520V3_QSPI_PCLK 13
+#define ZX297520V3_UART1_WCLK 14
+#define ZX297520V3_UART1_PCLK 15
+#define ZX297520V3_I2C1_WCLK 16
+#define ZX297520V3_I2C1_PCLK 17
+#define ZX297520V3_SPI0_WCLK 18
+#define ZX297520V3_SPI0_PCLK 19
+#define ZX297520V3_TIMER_LB_WCLK 20
+#define ZX297520V3_TIMER_LB_PCLK 21
+#define ZX297520V3_TIMER_LC_WCLK 22
+#define ZX297520V3_TIMER_LC_PCLK 23
+#define ZX297520V3_UART2_WCLK 24
+#define ZX297520V3_UART2_PCLK 25
+#define ZX297520V3_WDT_LE_WCLK 26
+#define ZX297520V3_WDT_LE_PCLK 27
+#define ZX297520V3_TIMER_LF_WCLK 28
+#define ZX297520V3_TIMER_LF_PCLK 29
+#define ZX297520V3_SPI1_WCLK 30
+#define ZX297520V3_SPI1_PCLK 31
+#define ZX297520V3_TIMER_L11_WCLK 32
+#define ZX297520V3_TIMER_L11_PCLK 33
+#define ZX297520V3_TDM_WCLK 34
+#define ZX297520V3_TDM_PCLK 35
+
#endif /* __DT_BINDINGS_CLOCK_ZX297520V3_H */
diff --git a/include/dt-bindings/reset/zte,zx297520v3-reset.h b/include/dt-bindings/reset/zte,zx297520v3-reset.h
index 4682d4a599c6..69eef84b2f29 100644
--- a/include/dt-bindings/reset/zte,zx297520v3-reset.h
+++ b/include/dt-bindings/reset/zte,zx297520v3-reset.h
@@ -41,4 +41,23 @@
#define ZX297520V3_VOU_RESET 9
#define ZX297520V3_LSP_RESET 10
+#define ZX297520V3_TIMER_L1_RESET 0
+#define ZX297520V3_WDT_L2_RESET 1
+#define ZX297520V3_WDT_L3_RESET 2
+#define ZX297520V3_PWM_RESET 3
+#define ZX297520V3_I2S0_RESET 4
+#define ZX297520V3_I2S1_RESET 5
+#define ZX297520V3_QSPI_RESET 6
+#define ZX297520V3_UART1_RESET 7
+#define ZX297520V3_I2C1_RESET 8
+#define ZX297520V3_SPI0_RESET 9
+#define ZX297520V3_TIMER_LB_RESET 10
+#define ZX297520V3_TIMER_LC_RESET 11
+#define ZX297520V3_UART2_RESET 12
+#define ZX297520V3_WDT_LE_RESET 13
+#define ZX297520V3_TIMER_LF_RESET 14
+#define ZX297520V3_SPI1_RESET 15
+#define ZX297520V3_TIMER_L11_RESET 16
+#define ZX297520V3_TDM_RESET 17
+
#endif /* __DT_BINDINGS_RESET_ZX297520V3_H */
--
2.54.0
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 50+ messages in thread* Re: [PATCH v8 03/12] dt-bindings: clk: zte: Add zx297520v3 LSP clock and reset controller
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 18:36 ` sashiko-bot
-1 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 18:36 UTC (permalink / raw)
To: Stefan Dösinger
Cc: neil.armstrong, robh, mfd, linux-phy, devicetree, vkoul, olteanv,
lee, conor+dt
> The clock controller of the zx297520v3 Low Speed Peripherals is
> relatively clean. One register per device with gates, muxes and resets
> and for some devices a divider. There are even bits in the matrix
> controller to control propagation of clock lines down to LSP.
>
> The clocks are sorted by register address and I am convinced that the
> device list is complete. There are, however, a few more registers that
> are likely extra dividers for TDM and I2S devices.
>
> Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>
> Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=3
^ permalink raw reply [flat|nested] 50+ messages in thread
* Re: [PATCH v8 03/12] dt-bindings: clk: zte: Add zx297520v3 LSP clock and reset controller
@ 2026-07-27 18:36 ` sashiko-bot
0 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 18:36 UTC (permalink / raw)
To: Stefan Dösinger
Cc: neil.armstrong, robh, mfd, linux-phy, devicetree, vkoul, olteanv,
lee, conor+dt
> The clock controller of the zx297520v3 Low Speed Peripherals is
> relatively clean. One register per device with gates, muxes and resets
> and for some devices a divider. There are even bits in the matrix
> controller to control propagation of clock lines down to LSP.
>
> The clocks are sorted by register address and I am convinced that the
> device list is complete. There are, however, a few more registers that
> are likely extra dividers for TDM and I2S devices.
>
> Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>
> Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=3
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 50+ messages in thread
* [PATCH v8 04/12] mfd: zx297520v3: Add a clock and reset MFD driver
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 18:24 ` Stefan Dösinger
-1 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
This driver registers child devices for the zx297520v3 clock and reset
controllers. The clk-zx297520v3 and reset-zte-zx297520v3 submitted in
the next patches will drive the respective functionalities.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
What I am still unsure about: How much sense does it make to have this
module-capable and able to unbind: In practice, the board doesn't work
without this driver and its clock and reset children.
Changes v8:
*) Remove .of_compatible from PHY mfd child
*) Move to drivers/mfd (Sashiko)
For me either soc/zte or mfd/ is fine. Note though that this MFD parent
is very specific to the zx297520v3 SoC. I don't expect this to be reused
anywhere else. There are two more MFD-ish devices in there: soc_sys at
0x140000, which I plan to handle in the same driver, and an i2c PMIC,
which would get its own host driver.
*) Use PLATFORM_DEVID_AUTO (Sashiko). NONE was intentional as I only
ever expect one instance, but I don't see any harm in doing the standard
thing and use AUTO
*) On the suggestion not to put the link to mfd_cells[] into match data:
I see that it is spelled out in Sashiko's mfd.md, written by Lee Jones,
the MFD maintainer. I would appreciate some education on the rationale
behind it: Putting a pointer into the void *data is a common pattern in
the kernel. I don't understand in which situation this can possibly
break?
Both structs are static const in the same compilation unit. data is a
const void *, not a uintptr_t or kernel_ulong_t, so it seems passing a
pointer is the intended use. What am I missing?
Changes v7:
Add phy MFD child
Changes v6: Make the ZTE SoC driver section depend on HAS_IOMEM
(Sashiko). The entire MFD section, which contains MFD_CORE, depends on
HAS_IOMEM even with COMPILE_TEST.
Add a NULL ptr check for of_device_get_match_data (Sashiko). While not
uniform, rave-sp, rohm-bd9576, atc260x, da9052-i2c protect against
incorrect manual attachment that way.
Add lspclk here as well in an attempt to satisfy both Conor Dooley, who
asks for MFD for top and matrix, and Philipp Zabel, who prefers aux but
at least wants the reset driver limited to one driver type.
Changes v5: Use MFD instead of Aux bus for top and matrix crm because of
extra functionality: Reboot in top, hwlock in Matrix.
LSP clocks stay with the aux bus and are thus not handled in this
driver. The clk driver will bind directly to the lspcrm node.
---
MAINTAINERS | 1 +
drivers/mfd/Kconfig | 11 +++++
drivers/mfd/Makefile | 2 +
drivers/mfd/zte-zx297520v3-crm.c | 98 ++++++++++++++++++++++++++++++++++++++++
4 files changed, 112 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 05322672b9b5..b04e9d43cfb0 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3883,6 +3883,7 @@ F: Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
F: Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
F: arch/arm/boot/dts/zte/
F: arch/arm/mach-zte/
+F: drivers/mfd/zte-zx297520v3-crm.c
F: include/dt-bindings/clock/zte,zx297520v3-clk.h
F: include/dt-bindings/phy/zte,zx297520v3-topcrm.h
F: include/dt-bindings/reset/zte,zx297520v3-reset.h
diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
index 35f6e9b76d05..c0c7e83487f4 100644
--- a/drivers/mfd/Kconfig
+++ b/drivers/mfd/Kconfig
@@ -2572,5 +2572,16 @@ config MFD_MAX7360
additional drivers must be enabled in order to use the functionality
of the device.
+config MFD_ZTE_ZX297520V3_CRM
+ tristate "ZTE zx297520v3 Clock and Reset Manager"
+ select MFD_CORE
+ default SOC_ZX297520V3
+ help
+ Say yes here to enable the driver for the ZTE zx297520v3 clock and
+ reset manager MFD driver. This driver provides the host device for
+ the clock and reset drivers and is required to boot the SoC. You
+ will also need to enable CLK_ZTE_ZX297520V3 and RESET_ZTE_ZX297520V3
+ to build the actual clock and reset child drivers.
+
endmenu
endif
diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
index dd4bb7e77c33..4f6a78170ac5 100644
--- a/drivers/mfd/Makefile
+++ b/drivers/mfd/Makefile
@@ -303,3 +303,5 @@ obj-$(CONFIG_MFD_RSMU_SPI) += rsmu_spi.o rsmu_core.o
obj-$(CONFIG_MFD_UPBOARD_FPGA) += upboard-fpga.o
obj-$(CONFIG_MFD_LOONGSON_SE) += loongson-se.o
+
+obj-$(CONFIG_MFD_ZTE_ZX297520V3_CRM) += zte-zx297520v3-crm.o
diff --git a/drivers/mfd/zte-zx297520v3-crm.c b/drivers/mfd/zte-zx297520v3-crm.c
new file mode 100644
index 000000000000..3a4f6cd0756e
--- /dev/null
+++ b/drivers/mfd/zte-zx297520v3-crm.c
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2026 Stefan Dösinger
+ */
+
+#include <linux/platform_device.h>
+#include <linux/mfd/core.h>
+#include <linux/module.h>
+#include <linux/errno.h>
+#include <linux/of.h>
+
+struct zx297520v3_crm_data {
+ const struct mfd_cell *cells;
+ unsigned int num_cells;
+};
+
+static const struct mfd_cell zx297520v3_topcrm_devs[] = {
+ {
+ .name = "zx297520v3-topclk",
+ },
+ {
+ .name = "zx297520v3-topreset",
+ },
+ {
+ .name = "syscon-reboot",
+ .of_compatible = "syscon-reboot",
+ },
+ {
+ .name = "zx297520v3-usb-phy",
+ },
+};
+
+static const struct zx297520v3_crm_data zx297520v3_topcrm_data = {
+ .cells = zx297520v3_topcrm_devs,
+ .num_cells = ARRAY_SIZE(zx297520v3_topcrm_devs),
+};
+
+static const struct mfd_cell zx297520v3_matrixcrm_devs[] = {
+ {
+ .name = "zx297520v3-matrixclk",
+ },
+ {
+ .name = "zx297520v3-matrixreset",
+ },
+ /* A set of hwlock controllers is found here as well, but no driver is implemented yet */
+};
+
+static const struct zx297520v3_crm_data zx297520v3_matrixcrm_data = {
+ .cells = zx297520v3_matrixcrm_devs,
+ .num_cells = ARRAY_SIZE(zx297520v3_matrixcrm_devs),
+};
+
+static const struct mfd_cell zx297520v3_lspcrm_devs[] = {
+ {
+ .name = "zx297520v3-lspclk",
+ },
+ {
+ .name = "zx297520v3-lspreset",
+ },
+};
+
+static const struct zx297520v3_crm_data zx297520v3_lspcrm_data = {
+ .cells = zx297520v3_lspcrm_devs,
+ .num_cells = ARRAY_SIZE(zx297520v3_lspcrm_devs),
+};
+
+static int zx297520v3_crm_probe(struct platform_device *pdev)
+{
+ const struct zx297520v3_crm_data *data;
+
+ data = of_device_get_match_data(&pdev->dev);
+ if (!data)
+ return -ENODEV;
+
+ return devm_mfd_add_devices(&pdev->dev, PLATFORM_DEVID_AUTO, data->cells,
+ data->num_cells, NULL, 0, NULL);
+}
+
+static const struct of_device_id of_match_zx297520v3_crm[] = {
+ { .compatible = "zte,zx297520v3-topcrm", .data = &zx297520v3_topcrm_data },
+ { .compatible = "zte,zx297520v3-matrixcrm", .data = &zx297520v3_matrixcrm_data },
+ { .compatible = "zte,zx297520v3-lspcrm", .data = &zx297520v3_lspcrm_data },
+ { }
+};
+MODULE_DEVICE_TABLE(of, of_match_zx297520v3_crm);
+
+static struct platform_driver zx297520v3_crm = {
+ .probe = zx297520v3_crm_probe,
+ .driver = {
+ .name = "zx297520v3-crm",
+ .of_match_table = of_match_zx297520v3_crm,
+ },
+};
+module_platform_driver(zx297520v3_crm);
+
+MODULE_AUTHOR("Stefan Dösinger <stefandoesinger@gmail.com>");
+MODULE_DESCRIPTION("ZTE zx297520v3 CRM MFD host driver");
+MODULE_LICENSE("GPL");
--
2.54.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* [PATCH v8 04/12] mfd: zx297520v3: Add a clock and reset MFD driver
@ 2026-07-27 18:24 ` Stefan Dösinger
0 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
This driver registers child devices for the zx297520v3 clock and reset
controllers. The clk-zx297520v3 and reset-zte-zx297520v3 submitted in
the next patches will drive the respective functionalities.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
What I am still unsure about: How much sense does it make to have this
module-capable and able to unbind: In practice, the board doesn't work
without this driver and its clock and reset children.
Changes v8:
*) Remove .of_compatible from PHY mfd child
*) Move to drivers/mfd (Sashiko)
For me either soc/zte or mfd/ is fine. Note though that this MFD parent
is very specific to the zx297520v3 SoC. I don't expect this to be reused
anywhere else. There are two more MFD-ish devices in there: soc_sys at
0x140000, which I plan to handle in the same driver, and an i2c PMIC,
which would get its own host driver.
*) Use PLATFORM_DEVID_AUTO (Sashiko). NONE was intentional as I only
ever expect one instance, but I don't see any harm in doing the standard
thing and use AUTO
*) On the suggestion not to put the link to mfd_cells[] into match data:
I see that it is spelled out in Sashiko's mfd.md, written by Lee Jones,
the MFD maintainer. I would appreciate some education on the rationale
behind it: Putting a pointer into the void *data is a common pattern in
the kernel. I don't understand in which situation this can possibly
break?
Both structs are static const in the same compilation unit. data is a
const void *, not a uintptr_t or kernel_ulong_t, so it seems passing a
pointer is the intended use. What am I missing?
Changes v7:
Add phy MFD child
Changes v6: Make the ZTE SoC driver section depend on HAS_IOMEM
(Sashiko). The entire MFD section, which contains MFD_CORE, depends on
HAS_IOMEM even with COMPILE_TEST.
Add a NULL ptr check for of_device_get_match_data (Sashiko). While not
uniform, rave-sp, rohm-bd9576, atc260x, da9052-i2c protect against
incorrect manual attachment that way.
Add lspclk here as well in an attempt to satisfy both Conor Dooley, who
asks for MFD for top and matrix, and Philipp Zabel, who prefers aux but
at least wants the reset driver limited to one driver type.
Changes v5: Use MFD instead of Aux bus for top and matrix crm because of
extra functionality: Reboot in top, hwlock in Matrix.
LSP clocks stay with the aux bus and are thus not handled in this
driver. The clk driver will bind directly to the lspcrm node.
---
MAINTAINERS | 1 +
drivers/mfd/Kconfig | 11 +++++
drivers/mfd/Makefile | 2 +
drivers/mfd/zte-zx297520v3-crm.c | 98 ++++++++++++++++++++++++++++++++++++++++
4 files changed, 112 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 05322672b9b5..b04e9d43cfb0 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3883,6 +3883,7 @@ F: Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
F: Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
F: arch/arm/boot/dts/zte/
F: arch/arm/mach-zte/
+F: drivers/mfd/zte-zx297520v3-crm.c
F: include/dt-bindings/clock/zte,zx297520v3-clk.h
F: include/dt-bindings/phy/zte,zx297520v3-topcrm.h
F: include/dt-bindings/reset/zte,zx297520v3-reset.h
diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
index 35f6e9b76d05..c0c7e83487f4 100644
--- a/drivers/mfd/Kconfig
+++ b/drivers/mfd/Kconfig
@@ -2572,5 +2572,16 @@ config MFD_MAX7360
additional drivers must be enabled in order to use the functionality
of the device.
+config MFD_ZTE_ZX297520V3_CRM
+ tristate "ZTE zx297520v3 Clock and Reset Manager"
+ select MFD_CORE
+ default SOC_ZX297520V3
+ help
+ Say yes here to enable the driver for the ZTE zx297520v3 clock and
+ reset manager MFD driver. This driver provides the host device for
+ the clock and reset drivers and is required to boot the SoC. You
+ will also need to enable CLK_ZTE_ZX297520V3 and RESET_ZTE_ZX297520V3
+ to build the actual clock and reset child drivers.
+
endmenu
endif
diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
index dd4bb7e77c33..4f6a78170ac5 100644
--- a/drivers/mfd/Makefile
+++ b/drivers/mfd/Makefile
@@ -303,3 +303,5 @@ obj-$(CONFIG_MFD_RSMU_SPI) += rsmu_spi.o rsmu_core.o
obj-$(CONFIG_MFD_UPBOARD_FPGA) += upboard-fpga.o
obj-$(CONFIG_MFD_LOONGSON_SE) += loongson-se.o
+
+obj-$(CONFIG_MFD_ZTE_ZX297520V3_CRM) += zte-zx297520v3-crm.o
diff --git a/drivers/mfd/zte-zx297520v3-crm.c b/drivers/mfd/zte-zx297520v3-crm.c
new file mode 100644
index 000000000000..3a4f6cd0756e
--- /dev/null
+++ b/drivers/mfd/zte-zx297520v3-crm.c
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2026 Stefan Dösinger
+ */
+
+#include <linux/platform_device.h>
+#include <linux/mfd/core.h>
+#include <linux/module.h>
+#include <linux/errno.h>
+#include <linux/of.h>
+
+struct zx297520v3_crm_data {
+ const struct mfd_cell *cells;
+ unsigned int num_cells;
+};
+
+static const struct mfd_cell zx297520v3_topcrm_devs[] = {
+ {
+ .name = "zx297520v3-topclk",
+ },
+ {
+ .name = "zx297520v3-topreset",
+ },
+ {
+ .name = "syscon-reboot",
+ .of_compatible = "syscon-reboot",
+ },
+ {
+ .name = "zx297520v3-usb-phy",
+ },
+};
+
+static const struct zx297520v3_crm_data zx297520v3_topcrm_data = {
+ .cells = zx297520v3_topcrm_devs,
+ .num_cells = ARRAY_SIZE(zx297520v3_topcrm_devs),
+};
+
+static const struct mfd_cell zx297520v3_matrixcrm_devs[] = {
+ {
+ .name = "zx297520v3-matrixclk",
+ },
+ {
+ .name = "zx297520v3-matrixreset",
+ },
+ /* A set of hwlock controllers is found here as well, but no driver is implemented yet */
+};
+
+static const struct zx297520v3_crm_data zx297520v3_matrixcrm_data = {
+ .cells = zx297520v3_matrixcrm_devs,
+ .num_cells = ARRAY_SIZE(zx297520v3_matrixcrm_devs),
+};
+
+static const struct mfd_cell zx297520v3_lspcrm_devs[] = {
+ {
+ .name = "zx297520v3-lspclk",
+ },
+ {
+ .name = "zx297520v3-lspreset",
+ },
+};
+
+static const struct zx297520v3_crm_data zx297520v3_lspcrm_data = {
+ .cells = zx297520v3_lspcrm_devs,
+ .num_cells = ARRAY_SIZE(zx297520v3_lspcrm_devs),
+};
+
+static int zx297520v3_crm_probe(struct platform_device *pdev)
+{
+ const struct zx297520v3_crm_data *data;
+
+ data = of_device_get_match_data(&pdev->dev);
+ if (!data)
+ return -ENODEV;
+
+ return devm_mfd_add_devices(&pdev->dev, PLATFORM_DEVID_AUTO, data->cells,
+ data->num_cells, NULL, 0, NULL);
+}
+
+static const struct of_device_id of_match_zx297520v3_crm[] = {
+ { .compatible = "zte,zx297520v3-topcrm", .data = &zx297520v3_topcrm_data },
+ { .compatible = "zte,zx297520v3-matrixcrm", .data = &zx297520v3_matrixcrm_data },
+ { .compatible = "zte,zx297520v3-lspcrm", .data = &zx297520v3_lspcrm_data },
+ { }
+};
+MODULE_DEVICE_TABLE(of, of_match_zx297520v3_crm);
+
+static struct platform_driver zx297520v3_crm = {
+ .probe = zx297520v3_crm_probe,
+ .driver = {
+ .name = "zx297520v3-crm",
+ .of_match_table = of_match_zx297520v3_crm,
+ },
+};
+module_platform_driver(zx297520v3_crm);
+
+MODULE_AUTHOR("Stefan Dösinger <stefandoesinger@gmail.com>");
+MODULE_DESCRIPTION("ZTE zx297520v3 CRM MFD host driver");
+MODULE_LICENSE("GPL");
--
2.54.0
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 50+ messages in thread* Re: [PATCH v8 04/12] mfd: zx297520v3: Add a clock and reset MFD driver
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 18:41 ` sashiko-bot
-1 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 18:41 UTC (permalink / raw)
To: Stefan Dösinger
Cc: vkoul, devicetree, mfd, olteanv, conor+dt, lee, neil.armstrong,
linux-phy, robh
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Platform data for child devices (`mfd_cell` arrays) is improperly passed as complex pointers via the `.data` field of the `of_device_id` match table.
--
--- Patch [4]: [PATCH v8 04/12] mfd: zx297520v3: Add a clock and reset MFD driver ---
commit 997b3ee44abc6ec239c36327d1c3fd90393542dd
Author: Stefan Dösinger <stefandoesinger@gmail.com>
mfd: zx297520v3: Add a clock and reset MFD driver
This driver registers child devices for the zx297520v3 clock and reset
controllers. The clk-zx297520v3 and reset-zte-zx297520v3 submitted in
the next patches will drive the respective functionalities.
> diff --git a/drivers/mfd/zte-zx297520v3-crm.c b/drivers/mfd/zte-zx297520v3-crm.c
> new file mode 100644
> index 0000000000000..3a4f6cd0756e3
> --- /dev/null
> +++ b/drivers/mfd/zte-zx297520v3-crm.c
[ ... ]
> +static int zx297520v3_crm_probe(struct platform_device *pdev)
> +{
> + const struct zx297520v3_crm_data *data;
> +
> + data = of_device_get_match_data(&pdev->dev);
> + if (!data)
> + return -ENODEV;
> +
> + return devm_mfd_add_devices(&pdev->dev, PLATFORM_DEVID_AUTO, data->cells,
> + data->num_cells, NULL, 0, NULL);
> +}
> +
> +static const struct of_device_id of_match_zx297520v3_crm[] = {
> + { .compatible = "zte,zx297520v3-topcrm", .data = &zx297520v3_topcrm_data },
> + { .compatible = "zte,zx297520v3-matrixcrm", .data = &zx297520v3_matrixcrm_data },
> + { .compatible = "zte,zx297520v3-lspcrm", .data = &zx297520v3_lspcrm_data },
[Severity: Medium]
Does passing platform data for child devices via the .data field as complex
pointers violate MFD subsystem architecture guidelines?
Instead of mapping compatible strings directly to zx297520v3_crm_data pointers
via the .data field, should this use an ID enum or integer ID, and have
zx297520v3_crm_probe() switch on the ID to select the correct static const
mfd_cell array?
> + { }
> +};
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=4
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 50+ messages in thread* Re: [PATCH v8 04/12] mfd: zx297520v3: Add a clock and reset MFD driver
@ 2026-07-27 18:41 ` sashiko-bot
0 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 18:41 UTC (permalink / raw)
To: Stefan Dösinger
Cc: vkoul, devicetree, mfd, olteanv, conor+dt, lee, neil.armstrong,
linux-phy, robh
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Platform data for child devices (`mfd_cell` arrays) is improperly passed as complex pointers via the `.data` field of the `of_device_id` match table.
--
--- Patch [4]: [PATCH v8 04/12] mfd: zx297520v3: Add a clock and reset MFD driver ---
commit 997b3ee44abc6ec239c36327d1c3fd90393542dd
Author: Stefan Dösinger <stefandoesinger@gmail.com>
mfd: zx297520v3: Add a clock and reset MFD driver
This driver registers child devices for the zx297520v3 clock and reset
controllers. The clk-zx297520v3 and reset-zte-zx297520v3 submitted in
the next patches will drive the respective functionalities.
> diff --git a/drivers/mfd/zte-zx297520v3-crm.c b/drivers/mfd/zte-zx297520v3-crm.c
> new file mode 100644
> index 0000000000000..3a4f6cd0756e3
> --- /dev/null
> +++ b/drivers/mfd/zte-zx297520v3-crm.c
[ ... ]
> +static int zx297520v3_crm_probe(struct platform_device *pdev)
> +{
> + const struct zx297520v3_crm_data *data;
> +
> + data = of_device_get_match_data(&pdev->dev);
> + if (!data)
> + return -ENODEV;
> +
> + return devm_mfd_add_devices(&pdev->dev, PLATFORM_DEVID_AUTO, data->cells,
> + data->num_cells, NULL, 0, NULL);
> +}
> +
> +static const struct of_device_id of_match_zx297520v3_crm[] = {
> + { .compatible = "zte,zx297520v3-topcrm", .data = &zx297520v3_topcrm_data },
> + { .compatible = "zte,zx297520v3-matrixcrm", .data = &zx297520v3_matrixcrm_data },
> + { .compatible = "zte,zx297520v3-lspcrm", .data = &zx297520v3_lspcrm_data },
[Severity: Medium]
Does passing platform data for child devices via the .data field as complex
pointers violate MFD subsystem architecture guidelines?
Instead of mapping compatible strings directly to zx297520v3_crm_data pointers
via the .data field, should this use an ID enum or integer ID, and have
zx297520v3_crm_probe() switch on the ID to select the correct static const
mfd_cell array?
> + { }
> +};
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=4
^ permalink raw reply [flat|nested] 50+ messages in thread
* [PATCH v8 05/12] clk: zte: Add Clock registration infrastructure
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 18:24 ` Stefan Dösinger
-1 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
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@gmail.com>
---
Version 8:
Use ZX297520V3_CLK_NO_EXPORT=(~0u) for unexported clocks. While using 0,
and starting clock indices at 1, is a common pattern in existing
drivers, it exposes a driver implementation detail in the hardware
binding interface.
If desired, I can change the special index to a separate field in the
structs.
Fix the return value if the ->init() callback fails. (Sashiko)
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 tell 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 | 91 +++++++++++++++++++++++++
drivers/clk/zte/pll-zx.c | 16 +++++
9 files changed, 320 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index b04e9d43cfb0..692f005cb2c4 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3883,6 +3883,7 @@ F: Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
F: Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
F: arch/arm/boot/dts/zte/
F: arch/arm/mach-zte/
+F: drivers/clk/zte/
F: drivers/mfd/zte-zx297520v3-crm.c
F: include/dt-bindings/clock/zte,zx297520v3-clk.h
F: include/dt-bindings/phy/zte,zx297520v3-topcrm.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@gmail.com>
+ */
+
+#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..c9e9048d3ade
--- /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 = 0, 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 != ZX297520V3_CLK_NO_EXPORT) {
+ 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 != ZX297520V3_CLK_NO_EXPORT) {
+ 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 res;
+ }
+
+ res = zx_clk_register_fixed_dividers(dev, map, data->fixed_divs, data->num_fixed_divs);
+ if (res)
+ return res;
+
+ res = zx_clk_register_plls(dev, map, data->plls, data->num_plls);
+ 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 = 0; 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@gmail.com>");
+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..90450da95941
--- /dev/null
+++ b/drivers/clk/zte/clk-zx.h
@@ -0,0 +1,91 @@
+/* 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
+#define ZX297520V3_CLK_NO_EXPORT (~0u)
+
+/* 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
^ permalink raw reply related [flat|nested] 50+ messages in thread* [PATCH v8 05/12] clk: zte: Add Clock registration infrastructure
@ 2026-07-27 18:24 ` Stefan Dösinger
0 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
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@gmail.com>
---
Version 8:
Use ZX297520V3_CLK_NO_EXPORT=(~0u) for unexported clocks. While using 0,
and starting clock indices at 1, is a common pattern in existing
drivers, it exposes a driver implementation detail in the hardware
binding interface.
If desired, I can change the special index to a separate field in the
structs.
Fix the return value if the ->init() callback fails. (Sashiko)
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 tell 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 | 91 +++++++++++++++++++++++++
drivers/clk/zte/pll-zx.c | 16 +++++
9 files changed, 320 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index b04e9d43cfb0..692f005cb2c4 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3883,6 +3883,7 @@ F: Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
F: Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
F: arch/arm/boot/dts/zte/
F: arch/arm/mach-zte/
+F: drivers/clk/zte/
F: drivers/mfd/zte-zx297520v3-crm.c
F: include/dt-bindings/clock/zte,zx297520v3-clk.h
F: include/dt-bindings/phy/zte,zx297520v3-topcrm.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@gmail.com>
+ */
+
+#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..c9e9048d3ade
--- /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 = 0, 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 != ZX297520V3_CLK_NO_EXPORT) {
+ 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 != ZX297520V3_CLK_NO_EXPORT) {
+ 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 res;
+ }
+
+ res = zx_clk_register_fixed_dividers(dev, map, data->fixed_divs, data->num_fixed_divs);
+ if (res)
+ return res;
+
+ res = zx_clk_register_plls(dev, map, data->plls, data->num_plls);
+ 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 = 0; 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@gmail.com>");
+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..90450da95941
--- /dev/null
+++ b/drivers/clk/zte/clk-zx.h
@@ -0,0 +1,91 @@
+/* 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
+#define ZX297520V3_CLK_NO_EXPORT (~0u)
+
+/* 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
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 50+ messages in thread* Re: [PATCH v8 05/12] clk: zte: Add Clock registration infrastructure
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 18:52 ` sashiko-bot
-1 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 18:52 UTC (permalink / raw)
To: Stefan Dösinger
Cc: devicetree, robh, lee, conor+dt, neil.armstrong, linux-phy, vkoul,
olteanv, mfd
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The ID sanity check incorrectly triggers a WARN_ON and fails the probe if the driver does not export any clocks.
--
--- Patch [5]: [PATCH v8 05/12] clk: zte: Add Clock registration infrastructure ---
commit a84018498091df350796aad9d7173588a10fa7ef
Author: Stefan Dösinger <stefandoesinger@gmail.com>
clk: zte: Add Clock registration infrastructure
This commit adds common clock registration infrastructure and helpers for
ZTE SoCs. It includes support for registering PLLs, muxes, dividers, and
gates, handling both exported and internal clocks.
> diff --git a/drivers/clk/zte/clk-zx.c b/drivers/clk/zte/clk-zx.c
> new file mode 100644
> index 0000000000000..c9e9048d3ade3
> --- /dev/null
> +++ b/drivers/clk/zte/clk-zx.c
[ ... ]
> +int zx_clk_common_probe(struct device *dev, struct device_node *of_node,
> + const struct zx_clk_data *data)
> +{
> + unsigned int public_clk_count = 0, 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 != ZX297520V3_CLK_NO_EXPORT) {
> + 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 != ZX297520V3_CLK_NO_EXPORT) {
> + 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;
[Severity: Medium]
Does this check unintentionally fail the probe if a driver doesn't export
any clocks?
Since highest_id is initialized to 0, if there are no exported clocks
public_clk_count will be 0. The check (0 != 0 + 1) evaluates to true,
triggering the WARN_ON and returning -EINVAL.
Perhaps highest_id could be initialized to ~0U instead, or a check could be
added to bypass this when public_clk_count is 0?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=5
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 50+ messages in thread* Re: [PATCH v8 05/12] clk: zte: Add Clock registration infrastructure
@ 2026-07-27 18:52 ` sashiko-bot
0 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 18:52 UTC (permalink / raw)
To: Stefan Dösinger
Cc: devicetree, robh, lee, conor+dt, neil.armstrong, linux-phy, vkoul,
olteanv, mfd
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The ID sanity check incorrectly triggers a WARN_ON and fails the probe if the driver does not export any clocks.
--
--- Patch [5]: [PATCH v8 05/12] clk: zte: Add Clock registration infrastructure ---
commit a84018498091df350796aad9d7173588a10fa7ef
Author: Stefan Dösinger <stefandoesinger@gmail.com>
clk: zte: Add Clock registration infrastructure
This commit adds common clock registration infrastructure and helpers for
ZTE SoCs. It includes support for registering PLLs, muxes, dividers, and
gates, handling both exported and internal clocks.
> diff --git a/drivers/clk/zte/clk-zx.c b/drivers/clk/zte/clk-zx.c
> new file mode 100644
> index 0000000000000..c9e9048d3ade3
> --- /dev/null
> +++ b/drivers/clk/zte/clk-zx.c
[ ... ]
> +int zx_clk_common_probe(struct device *dev, struct device_node *of_node,
> + const struct zx_clk_data *data)
> +{
> + unsigned int public_clk_count = 0, 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 != ZX297520V3_CLK_NO_EXPORT) {
> + 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 != ZX297520V3_CLK_NO_EXPORT) {
> + 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;
[Severity: Medium]
Does this check unintentionally fail the probe if a driver doesn't export
any clocks?
Since highest_id is initialized to 0, if there are no exported clocks
public_clk_count will be 0. The check (0 != 0 + 1) evaluates to true,
triggering the WARN_ON and returning -EINVAL.
Perhaps highest_id could be initialized to ~0U instead, or a check could be
added to bypass this when public_clk_count is 0?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=5
^ permalink raw reply [flat|nested] 50+ messages in thread
* [PATCH v8 06/12] clk: zte: Add regmap-based clocks
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 18:24 ` Stefan Dösinger
-1 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
This is based on meson/clk-regmap.c, although slightly simplified. I
have kept the copyright lines at the top of the file to indicate its
origin.
I see that numerous clock drivers have their own incarnation of regmap-
based mux/div/gate clocks. If there is any version of it that is likely
to be elevated to shared code like clk-gate.c I'll copy that and try to
use it as unmodified as possible.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
Version 8: Use ZX297520V3_CLK_NO_EXPORT instead of 0 for unexported clocks.
Version 6: Remove stray regmap (Sashiko)
Version 5: Use regmap_test_bits in zte_clk_regmap_gate_is_enabled
---
drivers/clk/zte/clk-regmap.c | 218 ++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 215 insertions(+), 3 deletions(-)
diff --git a/drivers/clk/zte/clk-regmap.c b/drivers/clk/zte/clk-regmap.c
index 984abeb45ab2..671b81bcda4d 100644
--- a/drivers/clk/zte/clk-regmap.c
+++ b/drivers/clk/zte/clk-regmap.c
@@ -7,28 +7,240 @@
*/
#include <linux/clk-provider.h>
+#include <linux/container_of.h>
#include <linux/device.h>
#include <linux/regmap.h>
+#include <linux/types.h>
#include <linux/errno.h>
+#include <linux/bits.h>
+#include <linux/err.h>
#include "clk-zx.h"
+struct zte_clk_regmap {
+ struct clk_hw hw;
+ struct regmap *map;
+ u16 reg;
+ u8 shift;
+ u8 size;
+};
+
+static inline struct zte_clk_regmap *to_zte_clk_regmap(struct clk_hw *hw)
+{
+ return container_of(hw, struct zte_clk_regmap, hw);
+}
+
+static int zte_clk_regmap_gate_enable(struct clk_hw *hw)
+{
+ struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+
+ return regmap_set_bits(clk->map, clk->reg, BIT(clk->shift));
+}
+
+static void zte_clk_regmap_gate_disable(struct clk_hw *hw)
+{
+ struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+
+ regmap_clear_bits(clk->map, clk->reg, BIT(clk->shift));
+}
+
+static int zte_clk_regmap_gate_is_enabled(struct clk_hw *hw)
+{
+ struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+
+ return regmap_test_bits(clk->map, clk->reg, BIT(clk->shift));
+}
+
+static const struct clk_ops zte_clk_regmap_gate_ops = {
+ .enable = zte_clk_regmap_gate_enable,
+ .disable = zte_clk_regmap_gate_disable,
+ .is_enabled = zte_clk_regmap_gate_is_enabled,
+};
+
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;
+ struct zte_clk_regmap *clk;
+ unsigned int i;
+ int res;
+
+ for (i = 0; i < num; ++i) {
+ struct clk_init_data init = {};
+
+ clk = devm_kzalloc(dev, sizeof(*clk), GFP_KERNEL);
+ if (!clk)
+ return -ENOMEM;
+
+ init.name = desc[i].name;
+ init.ops = &zte_clk_regmap_gate_ops;
+ init.parent_names = &desc[i].parent;
+ init.num_parents = 1;
+ init.flags = CLK_SET_RATE_PARENT | desc[i].flags;
+ clk->hw.init = &init;
+ clk->map = regmap;
+ clk->reg = desc[i].reg;
+ clk->shift = desc[i].shift;
+ clk->size = 1;
+
+ res = devm_clk_hw_register(dev, &clk->hw);
+ if (res)
+ return dev_err_probe(dev, res, "Failed to register clk %s\n", desc[i].name);
+
+ if (desc[i].id != ZX297520V3_CLK_NO_EXPORT)
+ clocks->hws[desc[i].id] = &clk->hw;
+ }
+
+ return 0;
+}
+
+static unsigned long zte_clk_regmap_div_recalc_rate(struct clk_hw *hw,
+ unsigned long prate)
+{
+ struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+ unsigned int val;
+ int ret;
+
+ ret = regmap_read(clk->map, clk->reg, &val);
+ if (ret)
+ /* Gives a hint that something is wrong */
+ return 0;
+
+ val >>= clk->shift;
+ val &= clk_div_mask(clk->size);
+ return divider_recalc_rate(hw, prate, val, NULL, 0, clk->size);
+}
+
+static int zte_clk_regmap_div_determine_rate(struct clk_hw *hw,
+ struct clk_rate_request *req)
+{
+ struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+
+ return divider_determine_rate(hw, req, NULL, clk->size, 0);
+}
+
+static int zte_clk_regmap_div_set_rate(struct clk_hw *hw, unsigned long rate,
+ unsigned long parent_rate)
+{
+ struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+ unsigned int val;
+ int ret;
+
+ ret = divider_get_val(rate, parent_rate, NULL, clk->size, 0);
+ if (ret < 0)
+ return ret;
+
+ val = (unsigned int)ret << clk->shift;
+ return regmap_update_bits(clk->map, clk->reg, clk_div_mask(clk->size) << clk->shift, val);
}
+static const struct clk_ops zte_clk_regmap_divider_ops = {
+ .recalc_rate = zte_clk_regmap_div_recalc_rate,
+ .determine_rate = zte_clk_regmap_div_determine_rate,
+ .set_rate = zte_clk_regmap_div_set_rate,
+};
+
int zx_clk_register_dividers(struct device *dev, struct regmap *regmap,
const struct zx_div_desc *desc, unsigned int num)
{
- return -ENODEV;
+ struct zte_clk_regmap *clk;
+ unsigned int i;
+ int res;
+
+ for (i = 0; i < num; ++i) {
+ struct clk_init_data init = {};
+
+ clk = devm_kzalloc(dev, sizeof(*clk), GFP_KERNEL);
+ if (!clk)
+ return -ENOMEM;
+
+ init.name = desc[i].name;
+ init.ops = &zte_clk_regmap_divider_ops;
+ init.parent_names = &desc[i].parent;
+ init.num_parents = 1;
+ init.flags = CLK_SET_RATE_PARENT;
+ clk->hw.init = &init;
+ clk->map = regmap;
+ clk->reg = desc[i].reg;
+ clk->shift = desc[i].shift;
+ clk->size = desc[i].size;
+
+ res = devm_clk_hw_register(dev, &clk->hw);
+ if (res)
+ return dev_err_probe(dev, res, "Failed to register clk %s\n", desc[i].name);
+ }
+
+ return 0;
}
+static u8 zte_clk_regmap_mux_get_parent(struct clk_hw *hw)
+{
+ struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+ unsigned int val;
+ int ret;
+
+ ret = regmap_read(clk->map, clk->reg, &val);
+ if (ret)
+ return 0xff;
+
+ val >>= clk->shift;
+ val &= GENMASK(clk->size - 1, 0);
+ return clk_mux_val_to_index(hw, NULL, 0, val);
+}
+
+static int zte_clk_regmap_mux_set_parent(struct clk_hw *hw, u8 index)
+{
+ struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+ unsigned int val = clk_mux_index_to_val(NULL, 0, index);
+
+ return regmap_update_bits(clk->map, clk->reg,
+ GENMASK(clk->size - 1, 0) << clk->shift,
+ val << clk->shift);
+}
+
+static int zte_clk_regmap_mux_determine_rate(struct clk_hw *hw, struct clk_rate_request *req)
+{
+ return clk_mux_determine_rate_flags(hw, req, 0);
+}
+
+static const struct clk_ops zte_clk_regmap_mux_ops = {
+ .get_parent = zte_clk_regmap_mux_get_parent,
+ .set_parent = zte_clk_regmap_mux_set_parent,
+ .determine_rate = zte_clk_regmap_mux_determine_rate,
+};
+
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;
+ struct zte_clk_regmap *clk;
+ unsigned int i;
+ int res;
+
+ for (i = 0; i < num; ++i) {
+ struct clk_init_data init = {};
+
+ clk = devm_kzalloc(dev, sizeof(*clk), GFP_KERNEL);
+ if (!clk)
+ return -ENOMEM;
+
+ init.name = desc[i].name;
+ init.ops = &zte_clk_regmap_mux_ops;
+ init.parent_names = desc[i].parents;
+ init.num_parents = desc[i].num_parents;
+ clk->hw.init = &init;
+ clk->map = regmap;
+ clk->reg = desc[i].reg;
+ clk->shift = desc[i].shift;
+ clk->size = desc[i].size;
+
+ res = devm_clk_hw_register(dev, &clk->hw);
+ if (res)
+ return dev_err_probe(dev, res, "Failed to register clk %s\n", desc[i].name);
+
+ if (desc[i].id != ZX297520V3_CLK_NO_EXPORT)
+ clocks->hws[desc[i].id] = &clk->hw;
+ }
+
+ return 0;
}
--
2.54.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* [PATCH v8 06/12] clk: zte: Add regmap-based clocks
@ 2026-07-27 18:24 ` Stefan Dösinger
0 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
This is based on meson/clk-regmap.c, although slightly simplified. I
have kept the copyright lines at the top of the file to indicate its
origin.
I see that numerous clock drivers have their own incarnation of regmap-
based mux/div/gate clocks. If there is any version of it that is likely
to be elevated to shared code like clk-gate.c I'll copy that and try to
use it as unmodified as possible.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
Version 8: Use ZX297520V3_CLK_NO_EXPORT instead of 0 for unexported clocks.
Version 6: Remove stray regmap (Sashiko)
Version 5: Use regmap_test_bits in zte_clk_regmap_gate_is_enabled
---
drivers/clk/zte/clk-regmap.c | 218 ++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 215 insertions(+), 3 deletions(-)
diff --git a/drivers/clk/zte/clk-regmap.c b/drivers/clk/zte/clk-regmap.c
index 984abeb45ab2..671b81bcda4d 100644
--- a/drivers/clk/zte/clk-regmap.c
+++ b/drivers/clk/zte/clk-regmap.c
@@ -7,28 +7,240 @@
*/
#include <linux/clk-provider.h>
+#include <linux/container_of.h>
#include <linux/device.h>
#include <linux/regmap.h>
+#include <linux/types.h>
#include <linux/errno.h>
+#include <linux/bits.h>
+#include <linux/err.h>
#include "clk-zx.h"
+struct zte_clk_regmap {
+ struct clk_hw hw;
+ struct regmap *map;
+ u16 reg;
+ u8 shift;
+ u8 size;
+};
+
+static inline struct zte_clk_regmap *to_zte_clk_regmap(struct clk_hw *hw)
+{
+ return container_of(hw, struct zte_clk_regmap, hw);
+}
+
+static int zte_clk_regmap_gate_enable(struct clk_hw *hw)
+{
+ struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+
+ return regmap_set_bits(clk->map, clk->reg, BIT(clk->shift));
+}
+
+static void zte_clk_regmap_gate_disable(struct clk_hw *hw)
+{
+ struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+
+ regmap_clear_bits(clk->map, clk->reg, BIT(clk->shift));
+}
+
+static int zte_clk_regmap_gate_is_enabled(struct clk_hw *hw)
+{
+ struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+
+ return regmap_test_bits(clk->map, clk->reg, BIT(clk->shift));
+}
+
+static const struct clk_ops zte_clk_regmap_gate_ops = {
+ .enable = zte_clk_regmap_gate_enable,
+ .disable = zte_clk_regmap_gate_disable,
+ .is_enabled = zte_clk_regmap_gate_is_enabled,
+};
+
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;
+ struct zte_clk_regmap *clk;
+ unsigned int i;
+ int res;
+
+ for (i = 0; i < num; ++i) {
+ struct clk_init_data init = {};
+
+ clk = devm_kzalloc(dev, sizeof(*clk), GFP_KERNEL);
+ if (!clk)
+ return -ENOMEM;
+
+ init.name = desc[i].name;
+ init.ops = &zte_clk_regmap_gate_ops;
+ init.parent_names = &desc[i].parent;
+ init.num_parents = 1;
+ init.flags = CLK_SET_RATE_PARENT | desc[i].flags;
+ clk->hw.init = &init;
+ clk->map = regmap;
+ clk->reg = desc[i].reg;
+ clk->shift = desc[i].shift;
+ clk->size = 1;
+
+ res = devm_clk_hw_register(dev, &clk->hw);
+ if (res)
+ return dev_err_probe(dev, res, "Failed to register clk %s\n", desc[i].name);
+
+ if (desc[i].id != ZX297520V3_CLK_NO_EXPORT)
+ clocks->hws[desc[i].id] = &clk->hw;
+ }
+
+ return 0;
+}
+
+static unsigned long zte_clk_regmap_div_recalc_rate(struct clk_hw *hw,
+ unsigned long prate)
+{
+ struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+ unsigned int val;
+ int ret;
+
+ ret = regmap_read(clk->map, clk->reg, &val);
+ if (ret)
+ /* Gives a hint that something is wrong */
+ return 0;
+
+ val >>= clk->shift;
+ val &= clk_div_mask(clk->size);
+ return divider_recalc_rate(hw, prate, val, NULL, 0, clk->size);
+}
+
+static int zte_clk_regmap_div_determine_rate(struct clk_hw *hw,
+ struct clk_rate_request *req)
+{
+ struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+
+ return divider_determine_rate(hw, req, NULL, clk->size, 0);
+}
+
+static int zte_clk_regmap_div_set_rate(struct clk_hw *hw, unsigned long rate,
+ unsigned long parent_rate)
+{
+ struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+ unsigned int val;
+ int ret;
+
+ ret = divider_get_val(rate, parent_rate, NULL, clk->size, 0);
+ if (ret < 0)
+ return ret;
+
+ val = (unsigned int)ret << clk->shift;
+ return regmap_update_bits(clk->map, clk->reg, clk_div_mask(clk->size) << clk->shift, val);
}
+static const struct clk_ops zte_clk_regmap_divider_ops = {
+ .recalc_rate = zte_clk_regmap_div_recalc_rate,
+ .determine_rate = zte_clk_regmap_div_determine_rate,
+ .set_rate = zte_clk_regmap_div_set_rate,
+};
+
int zx_clk_register_dividers(struct device *dev, struct regmap *regmap,
const struct zx_div_desc *desc, unsigned int num)
{
- return -ENODEV;
+ struct zte_clk_regmap *clk;
+ unsigned int i;
+ int res;
+
+ for (i = 0; i < num; ++i) {
+ struct clk_init_data init = {};
+
+ clk = devm_kzalloc(dev, sizeof(*clk), GFP_KERNEL);
+ if (!clk)
+ return -ENOMEM;
+
+ init.name = desc[i].name;
+ init.ops = &zte_clk_regmap_divider_ops;
+ init.parent_names = &desc[i].parent;
+ init.num_parents = 1;
+ init.flags = CLK_SET_RATE_PARENT;
+ clk->hw.init = &init;
+ clk->map = regmap;
+ clk->reg = desc[i].reg;
+ clk->shift = desc[i].shift;
+ clk->size = desc[i].size;
+
+ res = devm_clk_hw_register(dev, &clk->hw);
+ if (res)
+ return dev_err_probe(dev, res, "Failed to register clk %s\n", desc[i].name);
+ }
+
+ return 0;
}
+static u8 zte_clk_regmap_mux_get_parent(struct clk_hw *hw)
+{
+ struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+ unsigned int val;
+ int ret;
+
+ ret = regmap_read(clk->map, clk->reg, &val);
+ if (ret)
+ return 0xff;
+
+ val >>= clk->shift;
+ val &= GENMASK(clk->size - 1, 0);
+ return clk_mux_val_to_index(hw, NULL, 0, val);
+}
+
+static int zte_clk_regmap_mux_set_parent(struct clk_hw *hw, u8 index)
+{
+ struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+ unsigned int val = clk_mux_index_to_val(NULL, 0, index);
+
+ return regmap_update_bits(clk->map, clk->reg,
+ GENMASK(clk->size - 1, 0) << clk->shift,
+ val << clk->shift);
+}
+
+static int zte_clk_regmap_mux_determine_rate(struct clk_hw *hw, struct clk_rate_request *req)
+{
+ return clk_mux_determine_rate_flags(hw, req, 0);
+}
+
+static const struct clk_ops zte_clk_regmap_mux_ops = {
+ .get_parent = zte_clk_regmap_mux_get_parent,
+ .set_parent = zte_clk_regmap_mux_set_parent,
+ .determine_rate = zte_clk_regmap_mux_determine_rate,
+};
+
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;
+ struct zte_clk_regmap *clk;
+ unsigned int i;
+ int res;
+
+ for (i = 0; i < num; ++i) {
+ struct clk_init_data init = {};
+
+ clk = devm_kzalloc(dev, sizeof(*clk), GFP_KERNEL);
+ if (!clk)
+ return -ENOMEM;
+
+ init.name = desc[i].name;
+ init.ops = &zte_clk_regmap_mux_ops;
+ init.parent_names = desc[i].parents;
+ init.num_parents = desc[i].num_parents;
+ clk->hw.init = &init;
+ clk->map = regmap;
+ clk->reg = desc[i].reg;
+ clk->shift = desc[i].shift;
+ clk->size = desc[i].size;
+
+ res = devm_clk_hw_register(dev, &clk->hw);
+ if (res)
+ return dev_err_probe(dev, res, "Failed to register clk %s\n", desc[i].name);
+
+ if (desc[i].id != ZX297520V3_CLK_NO_EXPORT)
+ clocks->hws[desc[i].id] = &clk->hw;
+ }
+
+ return 0;
}
--
2.54.0
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 50+ messages in thread* Re: [PATCH v8 06/12] clk: zte: Add regmap-based clocks
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 19:03 ` sashiko-bot
-1 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 19:03 UTC (permalink / raw)
To: Stefan Dösinger
Cc: mfd, neil.armstrong, lee, linux-phy, devicetree, vkoul, robh,
olteanv, conor+dt
> This is based on meson/clk-regmap.c, although slightly simplified. I
> have kept the copyright lines at the top of the file to indicate its
> origin.
>
> I see that numerous clock drivers have their own incarnation of regmap-
> based mux/div/gate clocks. If there is any version of it that is likely
> to be elevated to shared code like clk-gate.c I'll copy that and try to
> use it as unmodified as possible.
>
> Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=6
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 50+ messages in thread
* Re: [PATCH v8 06/12] clk: zte: Add regmap-based clocks
@ 2026-07-27 19:03 ` sashiko-bot
0 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 19:03 UTC (permalink / raw)
To: Stefan Dösinger
Cc: mfd, neil.armstrong, lee, linux-phy, devicetree, vkoul, robh,
olteanv, conor+dt
> This is based on meson/clk-regmap.c, although slightly simplified. I
> have kept the copyright lines at the top of the file to indicate its
> origin.
>
> I see that numerous clock drivers have their own incarnation of regmap-
> based mux/div/gate clocks. If there is any version of it that is likely
> to be elevated to shared code like clk-gate.c I'll copy that and try to
> use it as unmodified as possible.
>
> Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=6
^ permalink raw reply [flat|nested] 50+ messages in thread
* [PATCH v8 07/12] clk: zte: Add zx PLL support infrastructure
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 18:24 ` Stefan Dösinger
-1 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
I am guessing how much of this is reusable among other zx chips or even
differently named ZTE platforms (if there are any). From reading the old
zx2967 code, I think the PLL code would be reusable there, maybe with
platform-specific bitmasks but otherwise the same logic.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
Version 8:
*) Document the behavior of unlocked PLLs better: They don't pass
through their reference/parent, but pass through the fixed clock-26m
oscillator, even if their reference clock is something else.
*) dpll has working fractionals. Add this in the comment, but there is
no actual code support for it - the LTE hardware doesn't need it.
As for Sashiko's comments on the .set_rate implementation: In practice
.set_rate will only ever set one rate, 491.52 MHz for dpll. All other
PLLs are bootloader configured. Dpll could be handled by writing a magic
constant into its config.
I want to have the rate finding code as documentation, and maybe there
is more elaborate future use for it (e.g. more flexible underclocking),
but attempts to handle eventualities like rate searches or misconfigured
bootloader values would be dead code.
Version 7:
*) Always keep unknownpll enabled when prepared so dpll can acquire a
lock in its prepare() function.
*) Clean up error reporting a bit (Sashiko)
Version 6:
*) Use abs_diff to compare target and candidate PLL rate (Sashiko).
*) Use req->best_parent_rate in zx29_pll_determine_rate. Add a TODO
comment about the parent rate flexibility.
Version 5: Fix some issues pointed out by Sashiko: NULL dev,
zx29_pll_recalc_rate error handling, disable PLL again on enable error.
---
drivers/clk/zte/pll-zx.c | 546 ++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 545 insertions(+), 1 deletion(-)
diff --git a/drivers/clk/zte/pll-zx.c b/drivers/clk/zte/pll-zx.c
index f8ddb4c7dff6..6e3a20cb5839 100644
--- a/drivers/clk/zte/pll-zx.c
+++ b/drivers/clk/zte/pll-zx.c
@@ -3,14 +3,558 @@
* Copyright (C) 2026 Stefan Dösinger
*/
#include <linux/clk-provider.h>
+#include <linux/container_of.h>
+#include <linux/rational.h>
#include <linux/device.h>
#include <linux/regmap.h>
+#include <linux/units.h>
+#include <linux/types.h>
#include <linux/errno.h>
+#include <linux/math.h>
+#include <linux/clk.h>
+#include <linux/err.h>
#include "clk-zx.h"
+/* This code has only been tested with zx297520v3 PLLs, but from reading the zx296718 clock code it
+ * looks like PLL registers are similar. ZTE's sources explain the PLL register contents only in a
+ * .cmm file (A Lauterbach TRACE32 script) and some unused headers in their U-Boot code dump, which
+ * may not be accurate. When calculating the frequencies from the default PLL configuration the
+ * results match the fixed rate clocks from their clock driver.
+ *
+ * The 26 MHz and 32 kHz clocks can be easily observed with the timers. The 104 MHz output can be
+ * observed through the UART. One 122.88 MHz clock can be observed through the TDM device. All
+ * others can only be indirectly inferred, e.g. by comparing CPU speed or SDIO transfer rate between
+ * the fixed 26 MHz oscillator and the provided PLL frequency.
+ *
+ * The formula to calculate the clock is ((ref / refdiv) * fbdiv) / postdiv1 / postdiv2. The masks
+ * are given below. There are a few control flags:
+ *
+ * Bit 31: Disables the PLL, but passes clock-26m through unmodified. Whether POSTDIV_OUT_DISABLE
+ * still matters is different between PLLs.
+ * Bit 30: Returns if the PLL is locked
+ * Bit 29: Not named in ZTE's code, but can be set. There is no obvious impact. Lock times are
+ * unchanged, so it doesn't influence or bypass lock detection. It doesn't raise any IRQs or
+ * influence GPIOs.
+ * Bit 27: Given its name it likely disables the Delta-Sigma Modulator, if one exists at all. The
+ * boot ROM sets it on every PLL. Unsetting it marginally decreases the time it takes to
+ * lock to the reference clock (from ~400 us to ~300 us).
+ * Bit 24: Bypasses the VCO, but still applies refdiv and postdiv. Doesn't matter if PLL_DISABLE=1.
+ *
+ * NB: Some PLLs have an automatic bypass logic that forwards clock-26m (REGARDLESS of reference)
+ * when they don't have a lock, regardless of reason. This can be triggered by disabling the PLL,
+ * setting an out-of-spec VCO frequency or disabling the parent. This shouldn't matter in regular
+ * operation, but caused me some confusion when reverse engineering the clock tree. E.g. clock-26m->
+ * unknownpll(disabled) -> dpll(enabled) counterintuitively results in a 26 MHz output clock.
+ */
+
+#define ZX29_PLL_DISABLE BIT(31)
+#define ZX29_PLL_LOCKED BIT(30)
+#define ZX29_PLL_LOCK_FILTER BIT(29)
+#define ZX29_PLL_DSM_DISABLE BIT(27)
+#define ZX29_PLL_PARENT_MASK GENMASK(26, 25)
+#define ZX29_PLL_PARENT_SHIFT 25
+#define ZX29_PLL_BYPASS BIT(24)
+#define ZX29_PLL_REFDIV_MASK GENMASK(23, 18)
+#define ZX29_PLL_REFDIV_SHIFT 18
+#define ZX29_PLL_FBDIV_MASK GENMASK(17, 6)
+#define ZX29_PLL_FBDIV_SHIFT 6
+#define ZX29_PLL_POSTDIV1_MASK GENMASK(5, 3)
+#define ZX29_PLL_POSTDIV1_SHIFT 3
+#define ZX29_PLL_POSTDIV2_MASK GENMASK(2, 0)
+#define ZX29_PLL_POSTDIV2_SHIFT 0
+
+/* The second register has a 24 bit fractional value, which only matters when ZX29_PLL_DSM_DISABLE
+ * is not set, and only seems to matter for dpll. ZTE's firmware does not make use of the fractional
+ * and it is unimplemented in this driver. Experimental testing confirms that it has an impact on
+ * dpll.
+ *
+ * Bits 27:24 contain more flags:
+ *
+ * Bit 27: Setting ZX29_PLL_DACAP slows down the lock time and obviates the speed gained from
+ * !DSM_DISABLE. No other effect observed.
+ *
+ * Bit 26: ZX29_PLL_4PHASE_OUT_DISABLE is set on some PLLs on boot but not on others. It is set on
+ * boot on mpll and upll, but not gpll, dpll or unknownpll. I am not sure what it does
+ * either. The SDIO devices break if they are fed from gpll with this flag set, but they
+ * work OK if they are fed from mpll without this flag set.
+ *
+ * Bit 25: ZX29_PLL_POSTDIV_OUT_DISABLE seems to disable the PLL output entirely. Whether it is
+ * bypassed by PLL_DISABLE differs between PLLs. gpll still produces an output clock if
+ * PLL_DISABLE = 1 and POSTDIV_DISABLE = 1, but produces no output if PLL_DISABLE = 0 and
+ * POSTDIV_DISABLE = 1. The dpll feeder ("unknownpll") at 0x100 produces no output clock
+ * if both PLL_DISABLE and POSTDIV_DISABLE are set to 1.
+ *
+ * Bit 24: ZX29_PLL_VCO_OUT_DISABLE probably disables the output of the VCO clock without
+ * post-VCO-dividers, but the raw VCO output is not a possible parent of any consumer clock,
+ * so I could not confirm this. It does not disable the VCO entirely - that's what
+ * PLL_DISABLE does.
+ *
+ * A spinlock should not be needed. PLLs don't share their registers with anything else and the
+ * global prepare mutex and enable spinlock should be enough. Beware of conflicts in reg2 between
+ * POSTDIV_OUT_DISABLE and the fractional value in case you find out how fractional dividers work
+ * and add support for them.
+ */
+#define ZX29_PLL_REG2_OFFSET 4
+#define ZX29_PLL_DACAP BIT(27)
+#define ZX29_PLL_4PHASE_OUT_DISABLE BIT(26)
+#define ZX29_PLL_POSTDIV_OUT_DISABLE BIT(25)
+#define ZX29_PLL_VCO_OUT_DISABLE BIT(24)
+#define ZX29_PLL_FRACT GENMASK(23, 0)
+
+/* The VCO's frequency range is limited. The stock settings run the VCO between 960 and 1248 MHz.
+ * Ad-hoc testing with gpll suggests that at least this PLL remains stable down to about 7 MHz and
+ * up to 2 GHz and produces a clock that can be used by the SDIO controller. Attempting to run the
+ * mpll VCO at 624 MHz and setting postdiv1 = postdiv2 = 1 - which should result in the same output
+ * frequency - or running it at 1872 MHz with an effective post divider of 3 crashes the CPU. Most
+ * likely the PLLs become unstable outside their core range and the SDIO controller is much more
+ * forgiving than CPU and DRAM are.
+ */
+#define ZX29_PLL_VCO_MAX_FREQ (1300 * HZ_PER_MHZ)
+#define ZX29_PLL_VCO_MIN_FREQ (900 * HZ_PER_MHZ)
+
+struct zx29_clk_pll {
+ struct clk_hw hw;
+ struct device *dev;
+ struct regmap *map;
+ u16 reg;
+};
+
+static inline struct zx29_clk_pll *to_zx29_clk_pll(struct clk_hw *hw)
+{
+ return container_of(hw, struct zx29_clk_pll, hw);
+}
+
+static int zx29_pll_is_prepared(struct clk_hw *hw)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ int res;
+
+ res = regmap_test_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
+ if (res < 0)
+ return res;
+
+ return !res;
+}
+
+static int zx29_pll_prepare(struct clk_hw *hw)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ u32 val = 0;
+ int res;
+
+ res = regmap_clear_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
+ if (res < 0)
+ return res;
+
+ /* Lock duration is usually between 300 us and 500 us */
+ res = regmap_read_poll_timeout(pll->map, pll->reg, val, val & ZX29_PLL_LOCKED, 50, 2000);
+ if (res) {
+ regmap_set_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
+ dev_err(pll->dev, "%s: PLL prepare failed: %d. Config value 0x%08x\n",
+ clk_hw_get_name(&pll->hw), res, val);
+ }
+ return res;
+}
+
+static void zx29_pll_unprepare(struct clk_hw *hw)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+
+ regmap_set_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
+}
+
+static int zx29_pll_is_enabled(struct clk_hw *hw)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ int res;
+
+ res = regmap_test_bits(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET,
+ ZX29_PLL_POSTDIV_OUT_DISABLE);
+ if (res < 0)
+ return res;
+
+ return !res;
+}
+
+static int zx29_pll_enable(struct clk_hw *hw)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+
+ return regmap_clear_bits(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET,
+ ZX29_PLL_POSTDIV_OUT_DISABLE);
+}
+
+static void zx29_pll_disable(struct clk_hw *hw)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+
+ regmap_set_bits(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET,
+ ZX29_PLL_POSTDIV_OUT_DISABLE);
+}
+
+static unsigned long zx29_pll_get_rate(const struct zx29_clk_pll *pll, unsigned long parent_rate,
+ u32 setting)
+{
+ unsigned long refdiv, fbdiv, postdiv1, postdiv2, freq;
+ const char *name = clk_hw_get_name(&pll->hw);
+ u64 vco;
+
+ refdiv = (setting & ZX29_PLL_REFDIV_MASK) >> ZX29_PLL_REFDIV_SHIFT;
+ fbdiv = (setting & ZX29_PLL_FBDIV_MASK) >> ZX29_PLL_FBDIV_SHIFT;
+ postdiv1 = (setting & ZX29_PLL_POSTDIV1_MASK) >> ZX29_PLL_POSTDIV1_SHIFT;
+ postdiv2 = (setting & ZX29_PLL_POSTDIV2_MASK) >> ZX29_PLL_POSTDIV2_SHIFT;
+ dev_dbg(pll->dev, "%s: reference clock %lu Hz, PLL setting 0x%08x\n",
+ name, parent_rate, setting);
+
+ if (!refdiv || !postdiv1 || !postdiv2) {
+ dev_err(pll->dev, "%s: divide by zero (%lu, %lu, %lu)\n", name, refdiv, postdiv1,
+ postdiv2);
+ return 0;
+ }
+
+ vco = div_u64((u64)parent_rate * fbdiv, refdiv);
+ freq = div_u64(div_u64(vco, postdiv1), postdiv2);
+ dev_dbg(pll->dev, "%s: refdiv %lu fbdiv %lu\n", name, refdiv, fbdiv);
+ dev_dbg(pll->dev, "%s: postdiv1 %lu postdiv2 %lu\n", name, postdiv1, postdiv2);
+
+ dev_dbg(pll->dev, "%s: %lu MHz\n", name, freq / HZ_PER_MHZ);
+
+ return freq;
+}
+
+static unsigned long zx29_pll_recalc_rate(struct clk_hw *hw, unsigned long parent_rate)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ u32 val;
+ int res;
+
+ res = regmap_read(pll->map, pll->reg, &val);
+ if (res < 0) {
+ dev_err(pll->dev, "%s: Failed to read PLL settings\n", clk_hw_get_name(&pll->hw));
+ return 0;
+ }
+
+ return zx29_pll_get_rate(pll, parent_rate, val);
+}
+
+static u32 zx29_pll_calc_values(const struct zx29_clk_pll *pll, unsigned long parent_rate,
+ unsigned long rate)
+{
+ const unsigned int postdiv1_max = (1 << hweight32(ZX29_PLL_POSTDIV1_MASK)) - 1;
+ const unsigned int postdiv2_max = (1 << hweight32(ZX29_PLL_POSTDIV2_MASK)) - 1;
+ unsigned long fbdiv, refdiv, best_fbdiv = 0, best_refdiv = 0;
+ u32 postdiv1 = 0, postdiv2 = 0, i, j, setting;
+ const char *name = clk_hw_get_name(&pll->hw);
+ long best = LONG_MAX;
+
+ /* This code produces the same VCO settings that the boot loader and stock firmware use for
+ * the standard frequencies. It has seen only very little manual testing beyond that.
+ *
+ * The goal is to find a VCO setting that gets us as close as possible to the desired output
+ * rate, while being within the VCO's operating limits and achievable with the input value
+ * range. It is iterating over possible post-VCO divider values (1-7)*(1-7) to look for
+ * valid VCO target frequencies and then looks for refdiv and fbdiv values to achieve the
+ * VCO frequency from the reference frequency.
+ */
+ for (j = 1; j <= postdiv2_max; j++) {
+ for (i = 1; i <= postdiv1_max; i++) {
+ u64 vco = (u64)rate * i * j;
+ unsigned long out;
+
+ if (vco > ZX29_PLL_VCO_MAX_FREQ || vco < ZX29_PLL_VCO_MIN_FREQ)
+ continue;
+
+ rational_best_approximation(vco, parent_rate,
+ (1 << hweight32(ZX29_PLL_FBDIV_MASK)) - 1,
+ (1 << hweight32(ZX29_PLL_REFDIV_MASK)) - 1,
+ &fbdiv, &refdiv);
+ setting = fbdiv << ZX29_PLL_FBDIV_SHIFT;
+ setting |= refdiv << ZX29_PLL_REFDIV_SHIFT;
+ setting |= i << ZX29_PLL_POSTDIV1_SHIFT;
+ setting |= j << ZX29_PLL_POSTDIV2_SHIFT;
+ out = zx29_pll_get_rate(pll, parent_rate, setting);
+
+ if (abs_diff(out, rate) > best)
+ continue;
+
+ if (abs_diff(out, rate) < best) {
+ postdiv1 = i;
+ postdiv2 = j;
+ best_fbdiv = fbdiv;
+ best_refdiv = refdiv;
+ best = abs_diff(out, rate);
+
+ if (!best)
+ goto search_done;
+ }
+ }
+ }
+search_done:
+
+ if (!postdiv1) {
+ dev_err(pll->dev, "Did not find a setting for %lu Hz, parent %lu Hz\n",
+ rate, parent_rate);
+ return 0;
+ }
+
+ dev_dbg(pll->dev, "%s: parent rate %lu\n", name, parent_rate);
+ dev_dbg(pll->dev, "%s: found VCO dividers %u and %u\n", name, postdiv1, postdiv2);
+ dev_dbg(pll->dev, "%s: VCO target rate %lu\n", name, rate * postdiv1 * postdiv2);
+
+ dev_dbg(pll->dev, "%s: Got fbdiv = %lu refdiv = %lu\n", name, best_fbdiv, best_refdiv);
+
+ setting = best_fbdiv << ZX29_PLL_FBDIV_SHIFT;
+ setting |= best_refdiv << ZX29_PLL_REFDIV_SHIFT;
+ setting |= postdiv1 << ZX29_PLL_POSTDIV1_SHIFT;
+ setting |= postdiv2 << ZX29_PLL_POSTDIV2_SHIFT;
+ dev_dbg(pll->dev, "%s: Final setting 0x%08x\n", name, setting);
+
+ return setting;
+}
+
+static int zx29_pll_determine_rate(struct clk_hw *hw, struct clk_rate_request *req)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ unsigned long new_rate;
+ u32 setting;
+
+ /* TODO: DPLL can switch between two parents, one of which is another PLL. Take this into
+ * account when searching the config space and set req->best_parent_rate.
+ *
+ * In practice it shouldn't matter though. Dpll is always configured to a fixed frequency
+ * and is the only clock with a switchable parent.
+ */
+ if (!req->best_parent_rate) {
+ dev_err(pll->dev, "Did not expect best_parent_rate=0\n");
+ return -EINVAL;
+ }
+
+ setting = zx29_pll_calc_values(pll, req->best_parent_rate, req->rate);
+ if (!setting)
+ return -EINVAL;
+
+ new_rate = zx29_pll_get_rate(pll, req->best_parent_rate, setting);
+ if (new_rate != req->rate) {
+ dev_warn(pll->dev, "Did not find an exact match. Want %lu, got %lu\n",
+ req->rate, new_rate);
+ req->rate = new_rate;
+ }
+
+ return 0;
+}
+
+static int zx29_pll_set_rate(struct clk_hw *hw, unsigned long rate,
+ unsigned long parent_rate)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ int res = -EINVAL;
+ u32 setting;
+
+ /* TODO: Implement gradual PLL rate change. PLLs can be changed while they are running and
+ * downstream hardware is generally fine with that. The exception is DRAM, which reads
+ * incorrect values if changed too fast.
+ *
+ * Changing the mpll rate is potentially useful for over/underclocking. Gating mpll is
+ * unrealistic because too many devices depend on it.
+ */
+ setting = zx29_pll_calc_values(pll, parent_rate, rate);
+ if (!setting)
+ return -EINVAL;
+
+ if (zx29_pll_get_rate(pll, parent_rate, setting) == rate) {
+ res = regmap_update_bits(pll->map, pll->reg, 0x00ffffff, setting);
+ dev_info(pll->dev, "%s: Setting rate: 0x%08x\n", clk_hw_get_name(hw), setting);
+ }
+
+ return res;
+}
+
+static u8 zx29_pll_get_parent(struct clk_hw *hw)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ u32 val;
+ int res;
+
+ res = regmap_read(pll->map, pll->reg, &val);
+ if (res < 0)
+ return 0xff;
+
+ val = (val & ZX29_PLL_PARENT_MASK) >> ZX29_PLL_PARENT_SHIFT;
+ dev_dbg(pll->dev, "%s: Parent 0x%x\n", clk_hw_get_name(hw), val);
+
+ return val;
+}
+
+static int zx29_pll_set_parent(struct clk_hw *hw, u8 index)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ u32 idx_shift = index << ZX29_PLL_PARENT_SHIFT;
+ int res;
+ u32 val;
+
+ res = regmap_update_bits(pll->map, pll->reg, ZX29_PLL_PARENT_MASK, idx_shift);
+ if (res < 0)
+ return res;
+
+ res = regmap_read(pll->map, pll->reg, &val);
+ if (res < 0)
+ return res;
+
+ if ((val & ZX29_PLL_PARENT_MASK) != idx_shift) {
+ dev_err(pll->dev, "Hardware rejected PLL parent %u\n", index);
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static int zx29_pll_init(struct clk_hw *hw)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ const char *name = clk_hw_get_name(hw);
+ int res;
+ u32 val;
+
+ dev_dbg(pll->dev, "%s: initializing\n", name);
+
+ /* The fractional value is not yet implemented. It works at least with dpll, but I have
+ * never seen it used on actual hardware.
+ */
+ res = regmap_read(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET, &val);
+ if (res < 0)
+ return res;
+ if (val & ZX29_PLL_FRACT) {
+ dev_warn(pll->dev, "%s: unsupported nonzero fractional value 0x%08lx\n",
+ name, val & ZX29_PLL_FRACT);
+ }
+
+ /* Remove the bypass flag so we don't have to bother with it in enable/disable. I have
+ * never seen it set by the earlier boot stages anyhow.
+ */
+ res = regmap_clear_bits(pll->map, pll->reg, ZX29_PLL_BYPASS);
+ if (res < 0)
+ return res;
+
+ res = regmap_test_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
+ if (res > 0) {
+ /* Set ZX29_PLL_POSTDIV_OUT_DISABLE for PLLs that have ZX29_PLL_DISABLE for
+ * consistency with .enable and .prepare. This ensures that .prepare doesn't
+ * inadvertently enable PLLs without .enable being called.
+ */
+ res = regmap_set_bits(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET,
+ ZX29_PLL_POSTDIV_OUT_DISABLE);
+ }
+
+ return res;
+}
+
+static const struct clk_ops zx29_pll_ops = {
+ .init = zx29_pll_init,
+ .is_prepared = zx29_pll_is_prepared,
+ .prepare = zx29_pll_prepare,
+ .unprepare = zx29_pll_unprepare,
+ .is_enabled = zx29_pll_is_enabled,
+ .enable = zx29_pll_enable,
+ .disable = zx29_pll_disable,
+ .recalc_rate = zx29_pll_recalc_rate,
+ .determine_rate = zx29_pll_determine_rate,
+ .get_parent = zx29_pll_get_parent,
+ .set_parent = zx29_pll_set_parent,
+ .set_rate = zx29_pll_set_rate,
+};
+
+/* A PLL that can be a parent of another PLL needs to produce an output signal when prepared,
+ * otherwise the downstream PLL's prepare() will fail to acquire a lock.
+ */
+static int zx29_pll_nodisable_init(struct clk_hw *hw)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ const char *name = clk_hw_get_name(hw);
+ int res;
+ u32 val;
+
+ dev_dbg(pll->dev, "%s: initializing, prepare-is-enabled\n", name);
+
+ res = regmap_read(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET, &val);
+ if (res < 0)
+ return res;
+ if (val & ZX29_PLL_FRACT) {
+ dev_warn(pll->dev, "%s: unsupported nonzero fractional value 0x%08lx\n",
+ name, val & ZX29_PLL_FRACT);
+ }
+
+ res = regmap_clear_bits(pll->map, pll->reg, ZX29_PLL_BYPASS);
+ if (res < 0)
+ return res;
+
+ return regmap_clear_bits(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET,
+ ZX29_PLL_POSTDIV_OUT_DISABLE);
+}
+
+static const struct clk_ops zx29_pll_nodisable_ops = {
+ .init = zx29_pll_nodisable_init,
+ .is_prepared = zx29_pll_is_prepared,
+ .prepare = zx29_pll_prepare,
+ .unprepare = zx29_pll_unprepare,
+ .recalc_rate = zx29_pll_recalc_rate,
+ .determine_rate = zx29_pll_determine_rate,
+ .get_parent = zx29_pll_get_parent,
+ .set_parent = zx29_pll_set_parent,
+ .set_rate = zx29_pll_set_rate,
+};
+
int zx_clk_register_plls(struct device *dev, struct regmap *regmap,
const struct zx_pll_desc *desc, unsigned int num)
{
- return -ENODEV;
+ struct zx29_clk_pll *pll;
+ unsigned int i;
+ int res;
+
+ for (i = 0; i < num; ++i) {
+ struct clk_init_data init = {};
+
+ pll = devm_kzalloc(dev, sizeof(*pll), GFP_KERNEL);
+ if (!pll)
+ return -ENOMEM;
+
+ if (desc[i].flags & ZX297520V3_PLL_PREPARE_IS_ENABLE)
+ init.ops = &zx29_pll_nodisable_ops;
+ else
+ init.ops = &zx29_pll_ops;
+
+ init.name = desc[i].name;
+ init.parent_names = desc[i].parents;
+ init.num_parents = desc[i].num_parents;
+ /* Disallow live reparent, but allow rate changes. PLLs can handle gradual changes
+ * fine, but large ones cause a temporary incorrect output, which can break
+ * downstream devices. See zx29_pll_set_rate for considerations on that.
+ */
+ init.flags = CLK_SET_PARENT_GATE;
+ pll->hw.init = &init;
+ pll->dev = dev;
+ pll->map = regmap;
+ pll->reg = desc[i].reg;
+
+ res = devm_clk_hw_register(dev, &pll->hw);
+ if (res)
+ return res;
+
+ /* Set the PLL rate only if the bootloader left it disabled. Keep the bootloader
+ * setup otherwise.
+ */
+ res = regmap_test_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
+ if (res < 0)
+ return res;
+
+ if (res > 0 && desc[i].rate) {
+ dev_dbg(pll->dev, "%s: Setting to %lu Hz\n", desc[i].name, desc[i].rate);
+ res = clk_set_rate(pll->hw.clk, desc[i].rate);
+ if (res) {
+ dev_err(dev, "%s: Failed to set rate.\n", desc[i].name);
+ return res;
+ }
+ }
+ }
+
+ return 0;
}
--
2.54.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* [PATCH v8 07/12] clk: zte: Add zx PLL support infrastructure
@ 2026-07-27 18:24 ` Stefan Dösinger
0 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
I am guessing how much of this is reusable among other zx chips or even
differently named ZTE platforms (if there are any). From reading the old
zx2967 code, I think the PLL code would be reusable there, maybe with
platform-specific bitmasks but otherwise the same logic.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
Version 8:
*) Document the behavior of unlocked PLLs better: They don't pass
through their reference/parent, but pass through the fixed clock-26m
oscillator, even if their reference clock is something else.
*) dpll has working fractionals. Add this in the comment, but there is
no actual code support for it - the LTE hardware doesn't need it.
As for Sashiko's comments on the .set_rate implementation: In practice
.set_rate will only ever set one rate, 491.52 MHz for dpll. All other
PLLs are bootloader configured. Dpll could be handled by writing a magic
constant into its config.
I want to have the rate finding code as documentation, and maybe there
is more elaborate future use for it (e.g. more flexible underclocking),
but attempts to handle eventualities like rate searches or misconfigured
bootloader values would be dead code.
Version 7:
*) Always keep unknownpll enabled when prepared so dpll can acquire a
lock in its prepare() function.
*) Clean up error reporting a bit (Sashiko)
Version 6:
*) Use abs_diff to compare target and candidate PLL rate (Sashiko).
*) Use req->best_parent_rate in zx29_pll_determine_rate. Add a TODO
comment about the parent rate flexibility.
Version 5: Fix some issues pointed out by Sashiko: NULL dev,
zx29_pll_recalc_rate error handling, disable PLL again on enable error.
---
drivers/clk/zte/pll-zx.c | 546 ++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 545 insertions(+), 1 deletion(-)
diff --git a/drivers/clk/zte/pll-zx.c b/drivers/clk/zte/pll-zx.c
index f8ddb4c7dff6..6e3a20cb5839 100644
--- a/drivers/clk/zte/pll-zx.c
+++ b/drivers/clk/zte/pll-zx.c
@@ -3,14 +3,558 @@
* Copyright (C) 2026 Stefan Dösinger
*/
#include <linux/clk-provider.h>
+#include <linux/container_of.h>
+#include <linux/rational.h>
#include <linux/device.h>
#include <linux/regmap.h>
+#include <linux/units.h>
+#include <linux/types.h>
#include <linux/errno.h>
+#include <linux/math.h>
+#include <linux/clk.h>
+#include <linux/err.h>
#include "clk-zx.h"
+/* This code has only been tested with zx297520v3 PLLs, but from reading the zx296718 clock code it
+ * looks like PLL registers are similar. ZTE's sources explain the PLL register contents only in a
+ * .cmm file (A Lauterbach TRACE32 script) and some unused headers in their U-Boot code dump, which
+ * may not be accurate. When calculating the frequencies from the default PLL configuration the
+ * results match the fixed rate clocks from their clock driver.
+ *
+ * The 26 MHz and 32 kHz clocks can be easily observed with the timers. The 104 MHz output can be
+ * observed through the UART. One 122.88 MHz clock can be observed through the TDM device. All
+ * others can only be indirectly inferred, e.g. by comparing CPU speed or SDIO transfer rate between
+ * the fixed 26 MHz oscillator and the provided PLL frequency.
+ *
+ * The formula to calculate the clock is ((ref / refdiv) * fbdiv) / postdiv1 / postdiv2. The masks
+ * are given below. There are a few control flags:
+ *
+ * Bit 31: Disables the PLL, but passes clock-26m through unmodified. Whether POSTDIV_OUT_DISABLE
+ * still matters is different between PLLs.
+ * Bit 30: Returns if the PLL is locked
+ * Bit 29: Not named in ZTE's code, but can be set. There is no obvious impact. Lock times are
+ * unchanged, so it doesn't influence or bypass lock detection. It doesn't raise any IRQs or
+ * influence GPIOs.
+ * Bit 27: Given its name it likely disables the Delta-Sigma Modulator, if one exists at all. The
+ * boot ROM sets it on every PLL. Unsetting it marginally decreases the time it takes to
+ * lock to the reference clock (from ~400 us to ~300 us).
+ * Bit 24: Bypasses the VCO, but still applies refdiv and postdiv. Doesn't matter if PLL_DISABLE=1.
+ *
+ * NB: Some PLLs have an automatic bypass logic that forwards clock-26m (REGARDLESS of reference)
+ * when they don't have a lock, regardless of reason. This can be triggered by disabling the PLL,
+ * setting an out-of-spec VCO frequency or disabling the parent. This shouldn't matter in regular
+ * operation, but caused me some confusion when reverse engineering the clock tree. E.g. clock-26m->
+ * unknownpll(disabled) -> dpll(enabled) counterintuitively results in a 26 MHz output clock.
+ */
+
+#define ZX29_PLL_DISABLE BIT(31)
+#define ZX29_PLL_LOCKED BIT(30)
+#define ZX29_PLL_LOCK_FILTER BIT(29)
+#define ZX29_PLL_DSM_DISABLE BIT(27)
+#define ZX29_PLL_PARENT_MASK GENMASK(26, 25)
+#define ZX29_PLL_PARENT_SHIFT 25
+#define ZX29_PLL_BYPASS BIT(24)
+#define ZX29_PLL_REFDIV_MASK GENMASK(23, 18)
+#define ZX29_PLL_REFDIV_SHIFT 18
+#define ZX29_PLL_FBDIV_MASK GENMASK(17, 6)
+#define ZX29_PLL_FBDIV_SHIFT 6
+#define ZX29_PLL_POSTDIV1_MASK GENMASK(5, 3)
+#define ZX29_PLL_POSTDIV1_SHIFT 3
+#define ZX29_PLL_POSTDIV2_MASK GENMASK(2, 0)
+#define ZX29_PLL_POSTDIV2_SHIFT 0
+
+/* The second register has a 24 bit fractional value, which only matters when ZX29_PLL_DSM_DISABLE
+ * is not set, and only seems to matter for dpll. ZTE's firmware does not make use of the fractional
+ * and it is unimplemented in this driver. Experimental testing confirms that it has an impact on
+ * dpll.
+ *
+ * Bits 27:24 contain more flags:
+ *
+ * Bit 27: Setting ZX29_PLL_DACAP slows down the lock time and obviates the speed gained from
+ * !DSM_DISABLE. No other effect observed.
+ *
+ * Bit 26: ZX29_PLL_4PHASE_OUT_DISABLE is set on some PLLs on boot but not on others. It is set on
+ * boot on mpll and upll, but not gpll, dpll or unknownpll. I am not sure what it does
+ * either. The SDIO devices break if they are fed from gpll with this flag set, but they
+ * work OK if they are fed from mpll without this flag set.
+ *
+ * Bit 25: ZX29_PLL_POSTDIV_OUT_DISABLE seems to disable the PLL output entirely. Whether it is
+ * bypassed by PLL_DISABLE differs between PLLs. gpll still produces an output clock if
+ * PLL_DISABLE = 1 and POSTDIV_DISABLE = 1, but produces no output if PLL_DISABLE = 0 and
+ * POSTDIV_DISABLE = 1. The dpll feeder ("unknownpll") at 0x100 produces no output clock
+ * if both PLL_DISABLE and POSTDIV_DISABLE are set to 1.
+ *
+ * Bit 24: ZX29_PLL_VCO_OUT_DISABLE probably disables the output of the VCO clock without
+ * post-VCO-dividers, but the raw VCO output is not a possible parent of any consumer clock,
+ * so I could not confirm this. It does not disable the VCO entirely - that's what
+ * PLL_DISABLE does.
+ *
+ * A spinlock should not be needed. PLLs don't share their registers with anything else and the
+ * global prepare mutex and enable spinlock should be enough. Beware of conflicts in reg2 between
+ * POSTDIV_OUT_DISABLE and the fractional value in case you find out how fractional dividers work
+ * and add support for them.
+ */
+#define ZX29_PLL_REG2_OFFSET 4
+#define ZX29_PLL_DACAP BIT(27)
+#define ZX29_PLL_4PHASE_OUT_DISABLE BIT(26)
+#define ZX29_PLL_POSTDIV_OUT_DISABLE BIT(25)
+#define ZX29_PLL_VCO_OUT_DISABLE BIT(24)
+#define ZX29_PLL_FRACT GENMASK(23, 0)
+
+/* The VCO's frequency range is limited. The stock settings run the VCO between 960 and 1248 MHz.
+ * Ad-hoc testing with gpll suggests that at least this PLL remains stable down to about 7 MHz and
+ * up to 2 GHz and produces a clock that can be used by the SDIO controller. Attempting to run the
+ * mpll VCO at 624 MHz and setting postdiv1 = postdiv2 = 1 - which should result in the same output
+ * frequency - or running it at 1872 MHz with an effective post divider of 3 crashes the CPU. Most
+ * likely the PLLs become unstable outside their core range and the SDIO controller is much more
+ * forgiving than CPU and DRAM are.
+ */
+#define ZX29_PLL_VCO_MAX_FREQ (1300 * HZ_PER_MHZ)
+#define ZX29_PLL_VCO_MIN_FREQ (900 * HZ_PER_MHZ)
+
+struct zx29_clk_pll {
+ struct clk_hw hw;
+ struct device *dev;
+ struct regmap *map;
+ u16 reg;
+};
+
+static inline struct zx29_clk_pll *to_zx29_clk_pll(struct clk_hw *hw)
+{
+ return container_of(hw, struct zx29_clk_pll, hw);
+}
+
+static int zx29_pll_is_prepared(struct clk_hw *hw)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ int res;
+
+ res = regmap_test_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
+ if (res < 0)
+ return res;
+
+ return !res;
+}
+
+static int zx29_pll_prepare(struct clk_hw *hw)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ u32 val = 0;
+ int res;
+
+ res = regmap_clear_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
+ if (res < 0)
+ return res;
+
+ /* Lock duration is usually between 300 us and 500 us */
+ res = regmap_read_poll_timeout(pll->map, pll->reg, val, val & ZX29_PLL_LOCKED, 50, 2000);
+ if (res) {
+ regmap_set_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
+ dev_err(pll->dev, "%s: PLL prepare failed: %d. Config value 0x%08x\n",
+ clk_hw_get_name(&pll->hw), res, val);
+ }
+ return res;
+}
+
+static void zx29_pll_unprepare(struct clk_hw *hw)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+
+ regmap_set_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
+}
+
+static int zx29_pll_is_enabled(struct clk_hw *hw)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ int res;
+
+ res = regmap_test_bits(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET,
+ ZX29_PLL_POSTDIV_OUT_DISABLE);
+ if (res < 0)
+ return res;
+
+ return !res;
+}
+
+static int zx29_pll_enable(struct clk_hw *hw)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+
+ return regmap_clear_bits(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET,
+ ZX29_PLL_POSTDIV_OUT_DISABLE);
+}
+
+static void zx29_pll_disable(struct clk_hw *hw)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+
+ regmap_set_bits(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET,
+ ZX29_PLL_POSTDIV_OUT_DISABLE);
+}
+
+static unsigned long zx29_pll_get_rate(const struct zx29_clk_pll *pll, unsigned long parent_rate,
+ u32 setting)
+{
+ unsigned long refdiv, fbdiv, postdiv1, postdiv2, freq;
+ const char *name = clk_hw_get_name(&pll->hw);
+ u64 vco;
+
+ refdiv = (setting & ZX29_PLL_REFDIV_MASK) >> ZX29_PLL_REFDIV_SHIFT;
+ fbdiv = (setting & ZX29_PLL_FBDIV_MASK) >> ZX29_PLL_FBDIV_SHIFT;
+ postdiv1 = (setting & ZX29_PLL_POSTDIV1_MASK) >> ZX29_PLL_POSTDIV1_SHIFT;
+ postdiv2 = (setting & ZX29_PLL_POSTDIV2_MASK) >> ZX29_PLL_POSTDIV2_SHIFT;
+ dev_dbg(pll->dev, "%s: reference clock %lu Hz, PLL setting 0x%08x\n",
+ name, parent_rate, setting);
+
+ if (!refdiv || !postdiv1 || !postdiv2) {
+ dev_err(pll->dev, "%s: divide by zero (%lu, %lu, %lu)\n", name, refdiv, postdiv1,
+ postdiv2);
+ return 0;
+ }
+
+ vco = div_u64((u64)parent_rate * fbdiv, refdiv);
+ freq = div_u64(div_u64(vco, postdiv1), postdiv2);
+ dev_dbg(pll->dev, "%s: refdiv %lu fbdiv %lu\n", name, refdiv, fbdiv);
+ dev_dbg(pll->dev, "%s: postdiv1 %lu postdiv2 %lu\n", name, postdiv1, postdiv2);
+
+ dev_dbg(pll->dev, "%s: %lu MHz\n", name, freq / HZ_PER_MHZ);
+
+ return freq;
+}
+
+static unsigned long zx29_pll_recalc_rate(struct clk_hw *hw, unsigned long parent_rate)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ u32 val;
+ int res;
+
+ res = regmap_read(pll->map, pll->reg, &val);
+ if (res < 0) {
+ dev_err(pll->dev, "%s: Failed to read PLL settings\n", clk_hw_get_name(&pll->hw));
+ return 0;
+ }
+
+ return zx29_pll_get_rate(pll, parent_rate, val);
+}
+
+static u32 zx29_pll_calc_values(const struct zx29_clk_pll *pll, unsigned long parent_rate,
+ unsigned long rate)
+{
+ const unsigned int postdiv1_max = (1 << hweight32(ZX29_PLL_POSTDIV1_MASK)) - 1;
+ const unsigned int postdiv2_max = (1 << hweight32(ZX29_PLL_POSTDIV2_MASK)) - 1;
+ unsigned long fbdiv, refdiv, best_fbdiv = 0, best_refdiv = 0;
+ u32 postdiv1 = 0, postdiv2 = 0, i, j, setting;
+ const char *name = clk_hw_get_name(&pll->hw);
+ long best = LONG_MAX;
+
+ /* This code produces the same VCO settings that the boot loader and stock firmware use for
+ * the standard frequencies. It has seen only very little manual testing beyond that.
+ *
+ * The goal is to find a VCO setting that gets us as close as possible to the desired output
+ * rate, while being within the VCO's operating limits and achievable with the input value
+ * range. It is iterating over possible post-VCO divider values (1-7)*(1-7) to look for
+ * valid VCO target frequencies and then looks for refdiv and fbdiv values to achieve the
+ * VCO frequency from the reference frequency.
+ */
+ for (j = 1; j <= postdiv2_max; j++) {
+ for (i = 1; i <= postdiv1_max; i++) {
+ u64 vco = (u64)rate * i * j;
+ unsigned long out;
+
+ if (vco > ZX29_PLL_VCO_MAX_FREQ || vco < ZX29_PLL_VCO_MIN_FREQ)
+ continue;
+
+ rational_best_approximation(vco, parent_rate,
+ (1 << hweight32(ZX29_PLL_FBDIV_MASK)) - 1,
+ (1 << hweight32(ZX29_PLL_REFDIV_MASK)) - 1,
+ &fbdiv, &refdiv);
+ setting = fbdiv << ZX29_PLL_FBDIV_SHIFT;
+ setting |= refdiv << ZX29_PLL_REFDIV_SHIFT;
+ setting |= i << ZX29_PLL_POSTDIV1_SHIFT;
+ setting |= j << ZX29_PLL_POSTDIV2_SHIFT;
+ out = zx29_pll_get_rate(pll, parent_rate, setting);
+
+ if (abs_diff(out, rate) > best)
+ continue;
+
+ if (abs_diff(out, rate) < best) {
+ postdiv1 = i;
+ postdiv2 = j;
+ best_fbdiv = fbdiv;
+ best_refdiv = refdiv;
+ best = abs_diff(out, rate);
+
+ if (!best)
+ goto search_done;
+ }
+ }
+ }
+search_done:
+
+ if (!postdiv1) {
+ dev_err(pll->dev, "Did not find a setting for %lu Hz, parent %lu Hz\n",
+ rate, parent_rate);
+ return 0;
+ }
+
+ dev_dbg(pll->dev, "%s: parent rate %lu\n", name, parent_rate);
+ dev_dbg(pll->dev, "%s: found VCO dividers %u and %u\n", name, postdiv1, postdiv2);
+ dev_dbg(pll->dev, "%s: VCO target rate %lu\n", name, rate * postdiv1 * postdiv2);
+
+ dev_dbg(pll->dev, "%s: Got fbdiv = %lu refdiv = %lu\n", name, best_fbdiv, best_refdiv);
+
+ setting = best_fbdiv << ZX29_PLL_FBDIV_SHIFT;
+ setting |= best_refdiv << ZX29_PLL_REFDIV_SHIFT;
+ setting |= postdiv1 << ZX29_PLL_POSTDIV1_SHIFT;
+ setting |= postdiv2 << ZX29_PLL_POSTDIV2_SHIFT;
+ dev_dbg(pll->dev, "%s: Final setting 0x%08x\n", name, setting);
+
+ return setting;
+}
+
+static int zx29_pll_determine_rate(struct clk_hw *hw, struct clk_rate_request *req)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ unsigned long new_rate;
+ u32 setting;
+
+ /* TODO: DPLL can switch between two parents, one of which is another PLL. Take this into
+ * account when searching the config space and set req->best_parent_rate.
+ *
+ * In practice it shouldn't matter though. Dpll is always configured to a fixed frequency
+ * and is the only clock with a switchable parent.
+ */
+ if (!req->best_parent_rate) {
+ dev_err(pll->dev, "Did not expect best_parent_rate=0\n");
+ return -EINVAL;
+ }
+
+ setting = zx29_pll_calc_values(pll, req->best_parent_rate, req->rate);
+ if (!setting)
+ return -EINVAL;
+
+ new_rate = zx29_pll_get_rate(pll, req->best_parent_rate, setting);
+ if (new_rate != req->rate) {
+ dev_warn(pll->dev, "Did not find an exact match. Want %lu, got %lu\n",
+ req->rate, new_rate);
+ req->rate = new_rate;
+ }
+
+ return 0;
+}
+
+static int zx29_pll_set_rate(struct clk_hw *hw, unsigned long rate,
+ unsigned long parent_rate)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ int res = -EINVAL;
+ u32 setting;
+
+ /* TODO: Implement gradual PLL rate change. PLLs can be changed while they are running and
+ * downstream hardware is generally fine with that. The exception is DRAM, which reads
+ * incorrect values if changed too fast.
+ *
+ * Changing the mpll rate is potentially useful for over/underclocking. Gating mpll is
+ * unrealistic because too many devices depend on it.
+ */
+ setting = zx29_pll_calc_values(pll, parent_rate, rate);
+ if (!setting)
+ return -EINVAL;
+
+ if (zx29_pll_get_rate(pll, parent_rate, setting) == rate) {
+ res = regmap_update_bits(pll->map, pll->reg, 0x00ffffff, setting);
+ dev_info(pll->dev, "%s: Setting rate: 0x%08x\n", clk_hw_get_name(hw), setting);
+ }
+
+ return res;
+}
+
+static u8 zx29_pll_get_parent(struct clk_hw *hw)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ u32 val;
+ int res;
+
+ res = regmap_read(pll->map, pll->reg, &val);
+ if (res < 0)
+ return 0xff;
+
+ val = (val & ZX29_PLL_PARENT_MASK) >> ZX29_PLL_PARENT_SHIFT;
+ dev_dbg(pll->dev, "%s: Parent 0x%x\n", clk_hw_get_name(hw), val);
+
+ return val;
+}
+
+static int zx29_pll_set_parent(struct clk_hw *hw, u8 index)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ u32 idx_shift = index << ZX29_PLL_PARENT_SHIFT;
+ int res;
+ u32 val;
+
+ res = regmap_update_bits(pll->map, pll->reg, ZX29_PLL_PARENT_MASK, idx_shift);
+ if (res < 0)
+ return res;
+
+ res = regmap_read(pll->map, pll->reg, &val);
+ if (res < 0)
+ return res;
+
+ if ((val & ZX29_PLL_PARENT_MASK) != idx_shift) {
+ dev_err(pll->dev, "Hardware rejected PLL parent %u\n", index);
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static int zx29_pll_init(struct clk_hw *hw)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ const char *name = clk_hw_get_name(hw);
+ int res;
+ u32 val;
+
+ dev_dbg(pll->dev, "%s: initializing\n", name);
+
+ /* The fractional value is not yet implemented. It works at least with dpll, but I have
+ * never seen it used on actual hardware.
+ */
+ res = regmap_read(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET, &val);
+ if (res < 0)
+ return res;
+ if (val & ZX29_PLL_FRACT) {
+ dev_warn(pll->dev, "%s: unsupported nonzero fractional value 0x%08lx\n",
+ name, val & ZX29_PLL_FRACT);
+ }
+
+ /* Remove the bypass flag so we don't have to bother with it in enable/disable. I have
+ * never seen it set by the earlier boot stages anyhow.
+ */
+ res = regmap_clear_bits(pll->map, pll->reg, ZX29_PLL_BYPASS);
+ if (res < 0)
+ return res;
+
+ res = regmap_test_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
+ if (res > 0) {
+ /* Set ZX29_PLL_POSTDIV_OUT_DISABLE for PLLs that have ZX29_PLL_DISABLE for
+ * consistency with .enable and .prepare. This ensures that .prepare doesn't
+ * inadvertently enable PLLs without .enable being called.
+ */
+ res = regmap_set_bits(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET,
+ ZX29_PLL_POSTDIV_OUT_DISABLE);
+ }
+
+ return res;
+}
+
+static const struct clk_ops zx29_pll_ops = {
+ .init = zx29_pll_init,
+ .is_prepared = zx29_pll_is_prepared,
+ .prepare = zx29_pll_prepare,
+ .unprepare = zx29_pll_unprepare,
+ .is_enabled = zx29_pll_is_enabled,
+ .enable = zx29_pll_enable,
+ .disable = zx29_pll_disable,
+ .recalc_rate = zx29_pll_recalc_rate,
+ .determine_rate = zx29_pll_determine_rate,
+ .get_parent = zx29_pll_get_parent,
+ .set_parent = zx29_pll_set_parent,
+ .set_rate = zx29_pll_set_rate,
+};
+
+/* A PLL that can be a parent of another PLL needs to produce an output signal when prepared,
+ * otherwise the downstream PLL's prepare() will fail to acquire a lock.
+ */
+static int zx29_pll_nodisable_init(struct clk_hw *hw)
+{
+ struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+ const char *name = clk_hw_get_name(hw);
+ int res;
+ u32 val;
+
+ dev_dbg(pll->dev, "%s: initializing, prepare-is-enabled\n", name);
+
+ res = regmap_read(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET, &val);
+ if (res < 0)
+ return res;
+ if (val & ZX29_PLL_FRACT) {
+ dev_warn(pll->dev, "%s: unsupported nonzero fractional value 0x%08lx\n",
+ name, val & ZX29_PLL_FRACT);
+ }
+
+ res = regmap_clear_bits(pll->map, pll->reg, ZX29_PLL_BYPASS);
+ if (res < 0)
+ return res;
+
+ return regmap_clear_bits(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET,
+ ZX29_PLL_POSTDIV_OUT_DISABLE);
+}
+
+static const struct clk_ops zx29_pll_nodisable_ops = {
+ .init = zx29_pll_nodisable_init,
+ .is_prepared = zx29_pll_is_prepared,
+ .prepare = zx29_pll_prepare,
+ .unprepare = zx29_pll_unprepare,
+ .recalc_rate = zx29_pll_recalc_rate,
+ .determine_rate = zx29_pll_determine_rate,
+ .get_parent = zx29_pll_get_parent,
+ .set_parent = zx29_pll_set_parent,
+ .set_rate = zx29_pll_set_rate,
+};
+
int zx_clk_register_plls(struct device *dev, struct regmap *regmap,
const struct zx_pll_desc *desc, unsigned int num)
{
- return -ENODEV;
+ struct zx29_clk_pll *pll;
+ unsigned int i;
+ int res;
+
+ for (i = 0; i < num; ++i) {
+ struct clk_init_data init = {};
+
+ pll = devm_kzalloc(dev, sizeof(*pll), GFP_KERNEL);
+ if (!pll)
+ return -ENOMEM;
+
+ if (desc[i].flags & ZX297520V3_PLL_PREPARE_IS_ENABLE)
+ init.ops = &zx29_pll_nodisable_ops;
+ else
+ init.ops = &zx29_pll_ops;
+
+ init.name = desc[i].name;
+ init.parent_names = desc[i].parents;
+ init.num_parents = desc[i].num_parents;
+ /* Disallow live reparent, but allow rate changes. PLLs can handle gradual changes
+ * fine, but large ones cause a temporary incorrect output, which can break
+ * downstream devices. See zx29_pll_set_rate for considerations on that.
+ */
+ init.flags = CLK_SET_PARENT_GATE;
+ pll->hw.init = &init;
+ pll->dev = dev;
+ pll->map = regmap;
+ pll->reg = desc[i].reg;
+
+ res = devm_clk_hw_register(dev, &pll->hw);
+ if (res)
+ return res;
+
+ /* Set the PLL rate only if the bootloader left it disabled. Keep the bootloader
+ * setup otherwise.
+ */
+ res = regmap_test_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
+ if (res < 0)
+ return res;
+
+ if (res > 0 && desc[i].rate) {
+ dev_dbg(pll->dev, "%s: Setting to %lu Hz\n", desc[i].name, desc[i].rate);
+ res = clk_set_rate(pll->hw.clk, desc[i].rate);
+ if (res) {
+ dev_err(dev, "%s: Failed to set rate.\n", desc[i].name);
+ return res;
+ }
+ }
+ }
+
+ return 0;
}
--
2.54.0
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 50+ messages in thread* Re: [PATCH v8 07/12] clk: zte: Add zx PLL support infrastructure
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 19:15 ` sashiko-bot
-1 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 19:15 UTC (permalink / raw)
To: Stefan Dösinger
Cc: lee, robh, mfd, vkoul, olteanv, neil.armstrong, conor+dt,
devicetree, linux-phy
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] zx29_pll_determine_rate ignores the req->min_rate and req->max_rate constraints provided by the clock framework, potentially violating bounds.
--
--- Patch [7]: [PATCH v8 07/12] clk: zte: Add zx PLL support infrastructure ---
commit b2281439469368ad41cdfc2e3b1cb9178840d858
Author: Stefan Dösinger <stefandoesinger@gmail.com>
clk: zte: Add zx PLL support infrastructure
This commit introduces the ZX PLL support infrastructure for ZTE platforms,
adding rate calculation, prepare/enable operations, and registration logic.
> diff --git a/drivers/clk/zte/pll-zx.c b/drivers/clk/zte/pll-zx.c
> index f8ddb4c7dff66..6e3a20cb5839a 100644
> --- a/drivers/clk/zte/pll-zx.c
> +++ b/drivers/clk/zte/pll-zx.c
[ ... ]
> +static u32 zx29_pll_calc_values(const struct zx29_clk_pll *pll, unsigned long parent_rate,
> + unsigned long rate)
> +{
[ ... ]
> + setting = fbdiv << ZX29_PLL_FBDIV_SHIFT;
> + setting |= refdiv << ZX29_PLL_REFDIV_SHIFT;
> + setting |= i << ZX29_PLL_POSTDIV1_SHIFT;
> + setting |= j << ZX29_PLL_POSTDIV2_SHIFT;
> + out = zx29_pll_get_rate(pll, parent_rate, setting);
> +
> + if (abs_diff(out, rate) > best)
> + continue;
> +
> + if (abs_diff(out, rate) < best) {
> + postdiv1 = i;
> + postdiv2 = j;
> + best_fbdiv = fbdiv;
[Severity: Medium]
Since this loop unconditionally picks the closest hardware frequency to the
requested rate, is it possible that it selects a frequency outside of the CCF
boundaries?
If a driver sets strict constraints via clk_set_rate_range(), and the nearest
PLL approximation falls outside that range while a valid alternative exists
inside it, clk_calc_new_rates() might reject the out-of-bounds rate.
[ ... ]
> +static int zx29_pll_determine_rate(struct clk_hw *hw, struct clk_rate_request *req)
> +{
> + struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
> + unsigned long new_rate;
> + u32 setting;
> +
> + /* TODO: DPLL can switch between two parents, one of which is another PLL. Take this into
> + * account when searching the config space and set req->best_parent_rate.
> + *
> + * In practice it shouldn't matter though. Dpll is always configured to a fixed frequency
> + * and is the only clock with a switchable parent.
> + */
> + if (!req->best_parent_rate) {
> + dev_err(pll->dev, "Did not expect best_parent_rate=0\n");
> + return -EINVAL;
> + }
> +
> + setting = zx29_pll_calc_values(pll, req->best_parent_rate, req->rate);
[Severity: Medium]
Does this code need to consider req->min_rate and req->max_rate before
calculating the new rate? The minimum and maximum boundaries passed by the
clock framework to restrict the search appear to be ignored here.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=7
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 50+ messages in thread* Re: [PATCH v8 07/12] clk: zte: Add zx PLL support infrastructure
@ 2026-07-27 19:15 ` sashiko-bot
0 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 19:15 UTC (permalink / raw)
To: Stefan Dösinger
Cc: lee, robh, mfd, vkoul, olteanv, neil.armstrong, conor+dt,
devicetree, linux-phy
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] zx29_pll_determine_rate ignores the req->min_rate and req->max_rate constraints provided by the clock framework, potentially violating bounds.
--
--- Patch [7]: [PATCH v8 07/12] clk: zte: Add zx PLL support infrastructure ---
commit b2281439469368ad41cdfc2e3b1cb9178840d858
Author: Stefan Dösinger <stefandoesinger@gmail.com>
clk: zte: Add zx PLL support infrastructure
This commit introduces the ZX PLL support infrastructure for ZTE platforms,
adding rate calculation, prepare/enable operations, and registration logic.
> diff --git a/drivers/clk/zte/pll-zx.c b/drivers/clk/zte/pll-zx.c
> index f8ddb4c7dff66..6e3a20cb5839a 100644
> --- a/drivers/clk/zte/pll-zx.c
> +++ b/drivers/clk/zte/pll-zx.c
[ ... ]
> +static u32 zx29_pll_calc_values(const struct zx29_clk_pll *pll, unsigned long parent_rate,
> + unsigned long rate)
> +{
[ ... ]
> + setting = fbdiv << ZX29_PLL_FBDIV_SHIFT;
> + setting |= refdiv << ZX29_PLL_REFDIV_SHIFT;
> + setting |= i << ZX29_PLL_POSTDIV1_SHIFT;
> + setting |= j << ZX29_PLL_POSTDIV2_SHIFT;
> + out = zx29_pll_get_rate(pll, parent_rate, setting);
> +
> + if (abs_diff(out, rate) > best)
> + continue;
> +
> + if (abs_diff(out, rate) < best) {
> + postdiv1 = i;
> + postdiv2 = j;
> + best_fbdiv = fbdiv;
[Severity: Medium]
Since this loop unconditionally picks the closest hardware frequency to the
requested rate, is it possible that it selects a frequency outside of the CCF
boundaries?
If a driver sets strict constraints via clk_set_rate_range(), and the nearest
PLL approximation falls outside that range while a valid alternative exists
inside it, clk_calc_new_rates() might reject the out-of-bounds rate.
[ ... ]
> +static int zx29_pll_determine_rate(struct clk_hw *hw, struct clk_rate_request *req)
> +{
> + struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
> + unsigned long new_rate;
> + u32 setting;
> +
> + /* TODO: DPLL can switch between two parents, one of which is another PLL. Take this into
> + * account when searching the config space and set req->best_parent_rate.
> + *
> + * In practice it shouldn't matter though. Dpll is always configured to a fixed frequency
> + * and is the only clock with a switchable parent.
> + */
> + if (!req->best_parent_rate) {
> + dev_err(pll->dev, "Did not expect best_parent_rate=0\n");
> + return -EINVAL;
> + }
> +
> + setting = zx29_pll_calc_values(pll, req->best_parent_rate, req->rate);
[Severity: Medium]
Does this code need to consider req->min_rate and req->max_rate before
calculating the new rate? The minimum and maximum boundaries passed by the
clock framework to restrict the search appear to be ignored here.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=7
^ permalink raw reply [flat|nested] 50+ messages in thread
* [PATCH v8 08/12] clk: zte: Introduce a driver for zx297520v3 top clocks
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 18:24 ` Stefan Dösinger
-1 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
This register space controls core devices: PLLs, the AHB bus, a lot of
timers, the USB controller, the Cortex M0 processor that boots the board
and a few other devices. For some reason the LTE coprocessor is also
partially controlled by it. The main application processor and DDR
memory are not found here though.
The register to reboot the board is found here, as well as a register to
control whether watchdog expiries cause a board reset.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
Version 8:
Rename the expected clock name of the fixed oscillators to clock-26m and
clock-32k. Sashiko pointed out that "oscXXm" is deprecated, although
widespread in existing drivers.
Add a define indirection for the AXI candidate=critical protection.
While it holds true in a system with running rprocs, I find it useful
for stress testing the clock tree to disable it.
Double-checked pclk and wclk bits, fixed incorrect assignments in
register 0x54.
Version 7:
The register at top+0x140 contains a series of gates that control PLL
input into the top controller and PLL and osc26m work clock forward into
matrix. This allowed further insight into the clock tree.
The PLL subdivision list was replaced with fixed factor clocks that
divide PLLs before and after the input gate (and some fixed factor divs
go into matrix in the next patch). The number of clocks exported to
Matrix is actually relatively small.
As for Sashiko's suggestion about out of bound parents for dpll and
unknownpll: I have never seen a parent != 0. If that happens on a device
I want to know about it. At this stage of development I'd rather have
clearly visible orphaned clock than silently continuing at an unexpected
frequency.
"osc26m" -> "clock-26m" as "oscXX", although widespread, is deprecated.
In doing so, change underscores in clock names to dashes (Sashiko).
Version 5:
*) Make it a MFD child driver instead of binding to the node directly
*) Correct parents for dpll
*) Correct HSIC work clock parent
---
drivers/clk/zte/Kconfig | 11 +
drivers/clk/zte/Makefile | 1 +
drivers/clk/zte/clk-zx297520v3.c | 576 +++++++++++++++++++++++++++++++++++++++
3 files changed, 588 insertions(+)
diff --git a/drivers/clk/zte/Kconfig b/drivers/clk/zte/Kconfig
index 0222549dd211..4a4b26c94a9d 100644
--- a/drivers/clk/zte/Kconfig
+++ b/drivers/clk/zte/Kconfig
@@ -14,3 +14,14 @@ config COMMON_CLK_ZTE
of this.
Enable this if you are building a kernel for a ZTE designed board.
+
+config CLK_ZTE_ZX297520V3
+ tristate "Clock driver for ZTE zx297520v3 based SoCs"
+ depends on COMMON_CLK_ZTE && (MFD_ZTE_ZX297520V3_CRM || COMPILE_TEST)
+ default SOC_ZX297520V3
+ help
+ This driver supports ZTE zx297520v3 basic clocks.
+
+ Enable this if you want to build a kernel that is able to run on
+ boards based on this SoC. You can safely enable multiple clock
+ drivers. The one(s) matching the device tree will be used.
diff --git a/drivers/clk/zte/Makefile b/drivers/clk/zte/Makefile
index 27db07293165..2c073512e919 100644
--- a/drivers/clk/zte/Makefile
+++ b/drivers/clk/zte/Makefile
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_COMMON_CLK_ZTE) += clk-zte.o
+obj-$(CONFIG_CLK_ZTE_ZX297520V3) += clk-zx297520v3.o
clk-zte-y += clk-zx.o pll-zx.o clk-regmap.o
diff --git a/drivers/clk/zte/clk-zx297520v3.c b/drivers/clk/zte/clk-zx297520v3.c
new file mode 100644
index 000000000000..4444c2eb1382
--- /dev/null
+++ b/drivers/clk/zte/clk-zx297520v3.c
@@ -0,0 +1,576 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2026 Stefan Dösinger
+ *
+ * There are no public datasheets for zx297520v3. The controls on this clock controller have been
+ * extracted from ZTE's kernel and U-Boot sources and a LOT of manual testing. Some clocks can be
+ * found in the power management (evb_cpurpm.img) and LTE (cpko.ko) blobs shipped with the routers.
+ *
+ * Because ZTE's sources are wrong or misleading in some parts and testing can be misinterpreted,
+ * this file contains a LOT of comments on how particular clocks were tested and how the impact of
+ * changes to them can be observed.
+ */
+#include <dt-bindings/clock/zte,zx297520v3-clk.h>
+#include <linux/platform_device.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+#include <linux/types.h>
+#include <linux/errno.h>
+
+#include "clk-zx.h"
+
+MODULE_IMPORT_NS("ZTE_CLK");
+
+/* AXI speed is either determined by a fixed selection in matrix+0x0, bits 0:2 or by a voting system
+ * with ballots in matrix+0x120, 0x124, 0x128. The voting system is enabled by setting matrix 0x12c
+ * to 1. Otherwise the selection in 0x0 applies.
+ *
+ * The fastest selected speed wins. This holds true for values 0 (26 MHz) and 7 (6.5 MHz) too, so it
+ * isn't a case of lowest selector wins. I haven't tested what happens if mpll is changed to output
+ * 5x the normal clock, so 7 beats 0. Presumably value 0 would still win the election.
+ *
+ * AXI speed 6.5 is a child of matrix-osc26m, not a PLL. This can be seen by gating it off in
+ * topcrm+0x140.
+ *
+ * The system initializes with voting disabled and all preferences set to the lowest setting, 7.
+ * The zx297520v3_matrix_init function transfers the global mux to our vote and enables voting.
+ *
+ * Because rprocs may change their selection without notice, all possible parents of the AXI mux are
+ * marked critical. This can however obscure bugs in the clock tree. Define ZX297520V3_AXI_CANDIDATE
+ * to 0 to disable this protection for testing and either not run anything on the rprocs or make
+ * sure Linux votes for the fastest rate.
+ */
+#define ZX297520V3_AXI_CANDIDATE CLK_IS_CRITICAL
+
+/* Used for gates where we don't know the parent input(s). Assume general oscillator */
+static const char clk_unknown[] = "clock-26m";
+
+/* For clocks that use the ungated clock-26m input */
+static const char * const clk_main[] = {
+ "clock-26m",
+};
+
+static const char * const dpll_parents[] = {
+ "unknownpll-d2",
+ "clock-26m",
+};
+
+static const char * const zx297520v3_top_inputs[] = {
+ "osc26m",
+ "osc32k"
+};
+
+/* Top and matrix clocks are chaotic - I haven't found a consistent pattern behind their register
+ * and bit locations. Generally there are two gates (pclk, wclk), one mux, two resets and sometimes
+ * one divider, but exceptions apply. For some devices there is only a reset and some general
+ * (parent) clocks need setup. This structure plus macro handles the somewhat regular parts.
+ *
+ * There are some patterns that can be observed.
+ * mux 0x3c, div 0x48, gate 0x54
+ * mux 0x40, div 0x4c, gate 0x5c
+ * mux 0x44, div 0x50, gate 0x60
+ *
+ * For a 0 - 0xc - 0x18 pattern. Muxes from 0x3c to 0x44, dividers from 0x48 to 0x50, gates 0x54 to
+ * 0x60. The pattern is broken for timer t17 though.
+ *
+ * Gates have 4 bits per clock - usually, but not always, bit 0 for wclk, bit 1 for pclk, bit 2 for
+ * something the ZTE kernel calls "gate" (the bits we use here are called "en"), which presumably
+ * enables automatic clock gating. Bit 3, if settable, seems unused. E.g. offset 0x54 accepts all
+ * bits in 0xF77F7F7F - suggesting RTC, I2C0 have an extra gate bit.
+ *
+ * The default mpll settings multiply the 26 MHz reference clock times 24. A mux selection of 26 MHz
+ * could mean using the 26 MHz oscillator directly, or passing it through the PLL and divide by 24.
+ *
+ * If a UART is set to mpll-d6 (default 104 MHz), changing the mpll multipliers does affect UART
+ * timing as it should. This does not happen when the UART is set to 26 MHz input or timers that
+ * read 26 MHz input. This suggests 26 MHz clocks use the reference clock directly.
+ *
+ * The general clock flow on this board goes from clock-26m into the PLLs. The PLL output is
+ * branched into a small number of divided frequencies (mpll, mpll-d4, mpll-d5, mpll-d6, mpll-d8,
+ * gpll, gpll-d2, gpll-d4) at a high level. These divided frequencies as well as clock-26m go
+ * through a series of gates at offset 0x140 in topcrm. The output of these gates provide wclks for
+ * everything.
+ *
+ * At least 3 of those gate outputs (mpll, dpll, gpll-d2) are further divided behind those gates,
+ * presumably in the matrix controller. There are multiple paths how 104 and 78 MHz are derived,
+ * some specific to topcrm (0x140 bits 1 and 4), some specific to matrix (0x140 bit 9). There are
+ * likely 3 "copies" of the 78 MHz clock and two 50 MHz ones.
+ *
+ * Pclk is not gated by the 0x140 gates at all. Likewise clock-32k is fed to the timers without
+ * passing through this gate, even for consumers on matrix/LSP. Topcrm clock-26m wclks are not gated
+ * either, but top-consumed PLL clocks are.
+ */
+
+static const struct zx_fixed_divider_desc zx297520v3_top_fixed_divs[] = {
+ /* clock-26m division for clk_outX */
+ {"clock-26m-d2", "clock-26m", 2},
+
+ /* This one is a parent for dpll */
+ {"unknownpll-d2", "unknownpll", 2},
+
+ /* Pre-0x140 divisions. They enter topcrm through different bits in 0x140 */
+ {"mpll-d2", "mpll", 2},
+ {"mpll-d5", "mpll", 5},
+ {"mpll-d6", "mpll", 6},
+ {"mpll-d8", "mpll", 8},
+ {"upll-d12", "upll", 12},
+ {"gpll-d2", "gpll", 2},
+ {"gpll-d4", "gpll", 4},
+
+ /* Post-140 divisions. They enter through the same bit and can be selected by different mux
+ * values of their consumers or get piped to multiple consumers with different documented
+ * rates - in particular, USB wclk (24 MHz according to ZTE) shares bit 1 with M0's 78 MHz.
+ */
+ {"top-mpll-d2-d4", "top-mpll-d2", 4},
+ {"top-mpll-d2-d13", "top-mpll-d2", 13},
+ {"top-upll-d12-d2", "top-upll-d12", 2},
+ {"top-gpll-d4-d2", "top-gpll-d4", 2},
+};
+
+/* AHB: The clock mux works and impact can be tested e.g. with iperf speed testing of the USB
+ * network connection. Values 2 and 3 give the same speed and depend on the same bit (3) in 0x140.
+ * The mpll-d6 rate is gated off by the same bit in 0x140 as m0's mpll-d6, but the d8 is different.
+ * This may indicate that one of the two uses a different clock rate than documented in ZTE's
+ * kernel. The M0 speed is easy to measure, so I am confident in the M0's 78 MHz rate. AHB looks
+ * like 78 rather than 52, but confidence is lower because RAM read tests are influenced by CPU
+ * overhead too.
+ */
+static const char * const ahb_sel[] = {
+ "clock-26m",
+ "top-mpll-d6", /* 104 MHz */
+ "top-mpll-d8", /* 78 MHz */
+ "top-mpll-d8", /* 78 MHz */
+};
+
+static const char * const timer_top_sel[] = {
+ "clock-32k",
+ "clock-26m",
+};
+
+static const char * const uart_top_sel[] = {
+ "clock-26m",
+ "top-mpll-d6", /* 104 MHz, sharing M0's and ahb's 0x140 gate */
+};
+
+/* The Cortex M0 coprocessor. It is responsible for booting the board and runs some power management
+ * helper code on the stock firmware. The M0 rproc itself is not critical, but most of topcrm's
+ * registers become unreadable if this is gated off or become slow when clocked at 32 kHz, e.g. GPIO
+ * becomes borderline unusable at 32 kHz.
+ *
+ * There is no dedicated M0 gate. Disabling ahb-wclk looks like the M0 stops from A53's point of
+ * view, but it merely breaks the A53's ability to communicate with it, and M0's ability to
+ * communicate with the standard UART on LSP. M0 keeps running with both ahb-wclk and ahb-pclk
+ * gated off and can re-enable the gates.
+ */
+static const char * const m0_sel[] = {
+ "clock-26m",
+ "top-mpll-d6", /* 104 MHz */
+ "top-mpll-d2-d4", /* 78 MHz, but different gate than the ahb counterpart */
+ "clock-32k", /* Yes, tested. It is SLLLLOOOOOWWW. */
+};
+
+/* Clk-out0/1/2/32k: These clocks are exposed on GPIOs 15, 16, 17 and 18 respectively. They are used
+ * in ZTE's camera and sound code, by directly poking into the clock registers from the device
+ * drivers. Until the respective devices are supported they can safely be switched off.
+ *
+ * For clk-out1 ZTE's camera code says the following:
+ *
+ * 0 -> 20 MHz
+ * 1 -> 40 MHz
+ * 2 -> 13 MHz
+ * 3 -> 26 MHz
+ *
+ * 0 and 1 read from upll. I confirmed their rates (upll-d24 and upll-d12) by setting upll to a very
+ * low frequency and sampling the clock by GPIO reads. The outputs of 2 and 3 are way too high to
+ * test that way. Neither is reading from any PLL, they keep oscillating when all PLLs are disabled.
+ * I am fairly sure that 3 is just direct clock-26m because it perfectly matches what out2 is
+ * showing in its 26 MHz setting.
+ *
+ * Clk-out2 is similar, but it has only one control bit in top 0x34 bit 8. Neither setting selects
+ * a PLL output. When setting *0x34 = 0x080, clk-out1 and clk-out2 are in lockstep, presumably
+ * running at 13 MHz. 0x1c0 also runs in lockstep, presumably 26 MHz.
+ *
+ * clk-out0 has a mux in bit 5. Value 0 most likely selects clock-26m. I am not entirely sure about
+ * value 1 (which is the default), but the m0 mux has an impact on it. It looks like a debug pin
+ * that exposes some core clock.
+ */
+
+static const char * const out0_sel[] = {
+ "clock-26m",
+ "m0-mux",
+};
+
+static const char * const out1_sel[] = {
+ "top-upll-d12-d2", /* 20 MHz */
+ "top-upll-d12", /* 40 MHz */
+ "clock-26m-d2",
+ "clock-26m",
+};
+
+static const char * const out2_sel[] = {
+ "clock-26m-d2",
+ "clock-26m",
+};
+
+/* Clk-o is similar to clk-out*, providing a clock on GPIO 20, presumably for driving a (R)MII phy.
+ * The 50 MHz value is documented in a stray comment in ZTE's GMAC driver. I tested it similarly to
+ * the above pins. Mux setting 0 gives half the count as setting 1 and setting 1 gives gpll divided
+ * by 4 - matching the 50 MHz suggested by the ZTE comment at the default gpll settings. Gating gpll
+ * stops the clock for values 0 and 1.
+ *
+ * Settings 2 and 3 are possible, but seem to return garbage. It is generally pulsing up and down,
+ * except if both gpll and upll are stopped. I suspect it just reads random electrical fluctuation
+ * from other places in the board. Yes, I had a pull-down enabled when testing this.
+ *
+ * I am not aware of any board that uses this though. The Ethernet-equipped ones I have all run the
+ * PHY with its own oscillator.
+ */
+static const char * const rmii_sel[] = {
+ "top-gpll-d4-d2", /* 25 MHz */
+ "top-gpll-d4", /* 50 MHz */
+};
+
+static const struct zx_pll_desc zx297520v3_plls[] = {
+ /* Default setting: 0x48040c11. 624/312/156. Only a single possible parent. This is the
+ * PLL for pretty much everything, including CPU, RAM and USB.
+ *
+ * Changing this PLL makes it possible to overclock the CPU or do more fine grained
+ * underclocking than the CPU's mux allows. It does run into two problems though: The USB
+ * device uses this PLL's output directly and is *very* sensitive to differences. DRAM
+ * is also fed by this clock and needs to be re-trained on larger changes, which needs to
+ * be done by the stage 1 boot loader.
+ */
+ {
+ .name = "mpll",
+ .parents = clk_main,
+ .num_parents = ARRAY_SIZE(clk_main),
+ .rate = 0,
+ .reg = 0x8
+ },
+
+ /* ZTE's code calls this PLL "upll". The only possible consumers I found are clk-out1,
+ * which outputs this clock on GPIO 16 and HSIC. HSIC doesn't have anything connected to
+ * it on the devices I tested. The device that consumes clk-out1 seems to be an SPI
+ * camera, which I haven't seen in any device so far. ZTE manipulates this PLL directly out
+ * of the camera code, so it is unlikely to have another consumer.
+ *
+ * Long story short, shut it off.
+ */
+ {
+ .name = "upll",
+ .parents = clk_main,
+ .num_parents = ARRAY_SIZE(clk_main),
+ .rate = 0,
+ .reg = 0x10,
+ },
+
+ /* Default value 0x4834902d. Feeds dpll. 46.08 MHz. Bit 25 can be set, so two parents are
+ * possible. It looks like both values select the 26 MHz oscillator though.
+ *
+ * Since dpll's prepare may depend on this clock generating a usable signal, it needs to be
+ * enabled when prepared.
+ */
+ {
+ .name = "unknownpll",
+ .parents = clk_main,
+ .num_parents = ARRAY_SIZE(clk_main),
+ .rate = 0,
+ .reg = 0x100,
+ .flags = ZX297520V3_PLL_PREPARE_IS_ENABLE,
+ },
+
+ /* The documentation says 491.52 MHz and measurement with the LSP TDM device supports this.
+ * The default value is 0x480c2011, but not all boot loaders set it up. To get to 491.52
+ * with these settings it needs a 23.04 MHz reference clock, which matches unknownpll-d2.
+ * If unknownpll is disabled, dpll loses its lock. We set the frequency on this PLL if we
+ * find it is not enabled by the boot loader.
+ *
+ * The proprietary LTE driver or rproc enables and disables it. TDM and I2S can use it.
+ *
+ * It accepts parent values 0, 1, 2 and 3. Parent 0 is unknownpll-d2. The others look like
+ * clock-26m. With a parent != 0 dpll never loses its lock even when all other PLLs are off
+ * and the TDM counter register increases at a rate consistent with a 26.0/23.04 clock
+ * increase.
+ */
+ {
+ .name = "dpll",
+ .parents = dpll_parents,
+ .num_parents = ARRAY_SIZE(dpll_parents),
+ .rate = 491520000,
+ .reg = 0x18,
+ },
+
+ /* "g" is either for "general" or "gigahertz". The VCO runs at 1 GHz. Output clocks are 200,
+ * 100, 50, 25, ... MHz. It is used optionally by SDIO and QSPI and can drive a GPIO clock
+ * output for RMII, so it doesn't seem very general.
+ */
+ {
+ .name = "gpll",
+ .parents = clk_main,
+ .num_parents = ARRAY_SIZE(clk_main),
+ .rate = 0,
+ .reg = 0x110,
+ },
+};
+
+#define MUX(_id, _name, _parents, _reg, _shift, _size) { \
+ _id, _name, _parents, ARRAY_SIZE(_parents), _reg, _shift, _size}
+
+#define DIV(_name, _parent, _reg, _shift, _size) { _name, _parent, _reg, _shift, _size }
+
+#define GATE(_id, _name, _parent, _reg, _shift, _flags) { \
+ .id = _id, \
+ .name = _name, \
+ .parent = _parent, \
+ .flags = _flags, \
+ .reg = _reg, \
+ .shift = _shift, \
+}
+
+static const struct zx_mux_desc zx297520v3_top_muxes[] = {
+ MUX(ZX297520V3_M0_WCLK, "m0-mux", m0_sel, 0x38, 0, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "ahb-mux", ahb_sel, 0x3c, 4, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-t08-mux", timer_top_sel, 0x40, 1, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-t09-mux", timer_top_sel, 0x40, 0, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-t12-mux", timer_top_sel, 0x3c, 0, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-t13-mux", timer_top_sel, 0x44, 0, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-t14-mux", timer_top_sel, 0x44, 1, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-t15-mux", timer_top_sel, 0x3c, 3, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-t16-mux", timer_top_sel, 0x44, 2, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-t17-mux", timer_top_sel, 0x120, 0, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "wdt-t18-mux", timer_top_sel, 0x3c, 6, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "i2c0-mux", uart_top_sel, 0x3c, 1, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "uart0-mux", uart_top_sel, 0x40, 2, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "out0-mux", out0_sel, 0x34, 5, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "out1-mux", out1_sel, 0x34, 6, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "out2-mux", out2_sel, 0x34, 8, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "rmiiphy-mux", rmii_sel, 0x11c, 0, 2),
+};
+
+static const struct zx_div_desc zx297520v3_top_dividers[] = {
+ DIV("timer-t08-div", "timer-t08-mux", 0x4c, 8, 4),
+ DIV("timer-t09-div", "timer-t09-mux", 0x4c, 0, 4),
+ DIV("timer-t12-div", "timer-t12-mux", 0x48, 0, 4),
+ DIV("timer-t13-div", "timer-t13-mux", 0x50, 0, 4),
+ DIV("timer-t14-div", "timer-t14-mux", 0x50, 4, 4),
+ DIV("timer-t15-div", "timer-t15-mux", 0x48, 4, 4),
+ DIV("timer-t16-div", "timer-t16-mux", 0x50, 8, 4),
+ DIV("timer-t17-div", "timer-t17-mux", 0x124, 0, 4),
+ DIV("wdt-t18-div", "wdt-t18-mux", 0x48, 8, 4),
+ DIV("usim1-div", clk_unknown, 0x48, 12, 1),
+};
+
+static const struct zx_gate_desc zx297520v3_top_gates[] = {
+ /* topcrm input gates. These are complicated, but shed a lot of light on the board's clock
+ * distribution.
+ *
+ * Settable mask: 0x017333FF. Generally one nibble per PLL, except for MPLL that has more.
+ * One for clock-26m work clock. pclk and clock-32k are not gated globally here or anywhere
+ * else as far as I can see.
+ *
+ * If you want to test your understanding of this board, try to manually configure it into a
+ * setup where bit 24 or bit 9 are off without crashing it.
+ */
+
+ /* Bit 0: No known consumer */
+ /* Bit 1: M0's 78 MHz selection, but somehow also involved in USB too */
+ GATE(ZX297520V3_CLK_NO_EXPORT, "top-mpll-d2", "mpll-d2", 0x140, 1, 0),
+ /* Bit 2: No known consumer, named "mpll_48m_clk" in ZTE's cpko.ko blob */
+ GATE(ZX297520V3_CLK_NO_EXPORT, "top-mpll-d8", "mpll-d8", 0x140, 3, 0),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "top-mpll-d6", "mpll-d6", 0x140, 4, 0),
+ /* Bit 5: No known consumer. "mpll_156m_clk" in cpko.ko */
+ GATE(ZX297520V3_MPLL_D5, "matrix-mpll-d5", "mpll-d5", 0x140, 6,
+ ZX297520V3_AXI_CANDIDATE),
+ /* Bit 7: No known consumer */
+ /* Bit 8: Has a consumer, LTE depends on it. But unknown what and how. Most likely one of
+ * the gsm clocks in topcrm+0x64 listed in cpko.ko.
+ */
+ /* Bit 9: Big MPLL provider for Matrix. It is an AXI voting candidate, but it is protected
+ * in Matrix's 0x118 gates.
+ */
+ GATE(ZX297520V3_MPLL, "matrix-mpll", "mpll", 0x140, 9, 0),
+ /* Bit 10: Always 0 */
+ /* Bit 11: Always 0 */
+ /* Bit 12: No known consumer. Existing patterns indicate a dpll consumer in top itself */
+ GATE(ZX297520V3_DPLL, "matrix-dpll", "dpll", 0x140, 13, 0),
+ /* Bit 14: Always 0 */
+ /* Bit 15: Always 0 */
+ GATE(ZX297520V3_CLK_NO_EXPORT, "top-upll-d12", "upll-d12", 0x140, 16, 0),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "top-upll", "upll", 0x140, 17, 0),
+ /* Bit 18: Always 0 */
+ /* Bit 19: Always 0 */
+ GATE(ZX297520V3_CLK_NO_EXPORT, "top-gpll-d4", "gpll-d4", 0x140, 20, 0),
+ GATE(ZX297520V3_GPLL_D2, "matrix-gpll-d2", "gpll-d2", 0x140, 21, 0),
+ GATE(ZX297520V3_GPLL, "matrix-gpll", "gpll", 0x140, 22, 0),
+ /* Bit 23: Always 0 */
+ /* Bit 24 covers all 26 MHz on matrix, but not pclks. Clock-26m consumers on topcrm do not
+ * use this, they get the ungated clock-26m. It is an AXI voting candidate, but does not
+ * need protection because it is the slowest speed. It will only be used if every processor,
+ * including us, votes for it.
+ */
+ GATE(ZX297520V3_GATED_OSC26M, "matrix-osc26m", "clock-26m", 0x140, 24, 0),
+
+ /* AHB clock gates: Disabling these cuts off the A53 from register access into 0x130000/
+ * 0x140000, including the topcrm region, so it can't be re-enabled. Mark them critical for
+ * that reason.
+ *
+ * Gating this off does not stop the M0 rproc, nor does it stop M0 from accessing topcrm. It
+ * does however gate off M0 from the AXI interconnect, so it can't read RAM, Matrix, etc. So
+ * these bits here merely gate the bus, not the actual devices.
+ */
+ GATE(ZX297520V3_AHB_PCLK, "ahb-pclk", "ahb-mux", 0x54, 12,
+ CLK_IS_CRITICAL),
+ GATE(ZX297520V3_AHB_WCLK, "ahb-wclk", "m0-mux", 0x54, 13,
+ CLK_IS_CRITICAL),
+
+ /* SRAM1 and 2 clocks. Leave them on for now, as turning them off carelessly hangs the M0 */
+ GATE(ZX297520V3_SRAM1_PCLK, "sram1-pclk", clk_main[0], 0x54, 18,
+ CLK_IS_CRITICAL),
+ GATE(ZX297520V3_SRAM2_PCLK, "sram2-pclk", clk_main[0], 0x54, 16,
+ CLK_IS_CRITICAL),
+
+ /* Pinmux (AON, TOP, IOCFG but not PDCFG). Critical as well until we have a driver that
+ * consumes it. I don't think we'll realistically shut this off ever.
+ *
+ * Setting either bit 0 or 1 in register 0x58 makes the device work.
+ */
+ GATE(ZX297520V3_PMM_WCLK, "pmm-wclk", clk_main[0], 0x58, 0,
+ CLK_IS_CRITICAL),
+ GATE(ZX297520V3_PMM_PCLK, "pmm-pclk", clk_main[0], 0x58, 1,
+ CLK_IS_CRITICAL),
+
+ /* Timers. We don't use any of them, just shut them off. The timers are named and sorted
+ * by the IO address of the main timer controls. Some of the controls are documented in
+ * ZTE's kernel, the others I found by trial and error. Timers in register 0x54 have the
+ * pclk first, the others have the wclk first.
+ *
+ * Timer T17 is used by the ZSP firmware. The rproc driver will enable it as needed.
+ */
+ GATE(ZX297520V3_TIMER_T08_WCLK, "timer-t08-wclk", "timer-t08-div", 0x5c, 8, 0),
+ GATE(ZX297520V3_TIMER_T08_PCLK, "timer-t08-pclk", clk_main[0], 0x5c, 9, 0),
+ GATE(ZX297520V3_TIMER_T09_WCLK, "timer-t09-wclk", "timer-t09-div", 0x5c, 4, 0),
+ GATE(ZX297520V3_TIMER_T09_PCLK, "timer-t09-pclk", clk_main[0], 0x5c, 5, 0),
+ GATE(ZX297520V3_TIMER_T12_WCLK, "timer-t12-wclk", "timer-t12-div", 0x54, 5, 0),
+ GATE(ZX297520V3_TIMER_T12_PCLK, "timer-t12-pclk", clk_main[0], 0x54, 4, 0),
+ GATE(ZX297520V3_TIMER_T13_WCLK, "timer-t13-wclk", "timer-t13-div", 0x60, 0, 0),
+ GATE(ZX297520V3_TIMER_T13_PCLK, "timer-t13-pclk", clk_main[0], 0x60, 1, 0),
+ GATE(ZX297520V3_TIMER_T14_WCLK, "timer-t14-wclk", "timer-t14-div", 0x60, 4, 0),
+ GATE(ZX297520V3_TIMER_T14_PCLK, "timer-t14-pclk", clk_main[0], 0x60, 5, 0),
+ GATE(ZX297520V3_TIMER_T15_WCLK, "timer-t15-wclk", "timer-t15-div", 0x54, 21, 0),
+ GATE(ZX297520V3_TIMER_T15_PCLK, "timer-t15-pclk", clk_main[0], 0x54, 20, 0),
+ GATE(ZX297520V3_TIMER_T16_WCLK, "timer-t16-wclk", "timer-t16-div", 0x60, 8, 0),
+ GATE(ZX297520V3_TIMER_T16_PCLK, "timer-t16-pclk", clk_main[0], 0x60, 9, 0),
+ GATE(ZX297520V3_TIMER_T17_WCLK, "timer-t17-wclk", "timer-t17-div", 0x128, 0, 0),
+ GATE(ZX297520V3_TIMER_T17_PCLK, "timer-t17-pclk", clk_main[0], 0x128, 1, 0),
+ /* This watchdog is set up by the bootloader and in normal operation the m0 firmware will
+ * feed the dog. The m0 firmware in turn wants to be fed in its own way. Since we normally
+ * don't run any m0 firmware we shut it off by default and expose it to userspace via the
+ * watchdog driver.
+ */
+ GATE(ZX297520V3_WDT_T18_WCLK, "wdt-t18-wclk", "wdt-t18-div", 0x54, 25, 0),
+ GATE(ZX297520V3_WDT_T18_PCLK, "wdt-t18-pclk", clk_main[0], 0x54, 24, 0),
+
+ GATE(ZX297520V3_I2C0_WCLK, "i2c0-wclk", "i2c0-mux", 0x54, 9, 0),
+ GATE(ZX297520V3_I2C0_PCLK, "i2c0-pclk", clk_main[0], 0x54, 8, 0),
+ GATE(ZX297520V3_UART0_WCLK, "uart0-wclk", "uart0-mux", 0x5c, 12, 0),
+ GATE(ZX297520V3_UART0_PCLK, "uart0-pclk", clk_main[0], 0x5c, 13, 0),
+
+ /* ZTE says the USB input is a 24 MHz clock based on mpll. Testing shows that Upll is not
+ * involved. The USB register space can be accessed with ahb-pclk gated, but not with
+ * ahb-wclk gated. ZTE also lists ahb-wclk as parent for the second clock.
+ *
+ * There doesn't seem to be a separate PHY clock. usb-wclk stops controller registers from
+ * updating, but doesn't cause the USB device to disconnect like asserting the PHY reset
+ * does. Bit 0 in this register can't be set, so no PHY clock is hiding there either.
+ */
+ GATE(ZX297520V3_USB_WCLK, "usb-wclk", "top-mpll-d2-d13", 0x6c, 3, 0),
+ GATE(ZX297520V3_USB_PCLK, "usb-pclk", "ahb-wclk", 0x6c, 4, 0),
+ /* The HSIC hardware is listed in ZTE's code with a physical address of 0x01600000. Unlike
+ * the USB controller, it does consume upll. ZTE says 240 MHz, but 480 is the usual one for
+ * HSIC.
+ */
+ GATE(ZX297520V3_HSIC_WCLK, "hsic-wclk", "top-upll", 0x6c, 1, 0),
+ GATE(ZX297520V3_HSIC_PCLK, "hsic-pclk", "ahb-wclk", 0x6c, 2, 0),
+
+ /* How does this RTC work? I don't know, the ZTE kernel does not talk to it. The actual RTC
+ * is on the I2C-connected PMIC.
+ */
+ GATE(ZX297520V3_RTC_WCLK, "rtc-wclk", clk_unknown, 0x54, 1, 0),
+ GATE(ZX297520V3_RTC_PCLK, "rtc-pclk", clk_main[0], 0x54, 0, 0),
+
+ GATE(ZX297520V3_USIM1_WCLK, "usim1-wclk", "usim1-div", 0x54, 29, 0),
+ GATE(ZX297520V3_USIM1_PCLK, "usim1-pclk", clk_main[0], 0x54, 28, 0),
+
+ /* LTE: gate only as far as I can see. I looked for resets and did not find any. There may
+ * be mux/div, but without understanding the behavior of this hardware it is impossible to
+ * tell. They are sorted by physical MMIO address of the devices, which happens to be the
+ * inverse order of the bits.
+ *
+ * I don't know what "LPM", "TD" and "W" mean. I copied them from ZTE's names.
+ *
+ * Like PMM in this register, either bit enables register access, whether they control pclk
+ * or wclk cannot be distinguished.
+ */
+ GATE(ZX297520V3_LPM_GSM_WCLK, "lpm-gsm-wclk", clk_unknown, 0x58, 10, 0),
+ GATE(ZX297520V3_LPM_GSM_PCLK, "lpm-gsm-pclk", clk_main[0], 0x58, 11, 0),
+ GATE(ZX297520V3_LPM_LTE_WCLK, "lpm-lte-wclk", clk_unknown, 0x58, 8, 0),
+ GATE(ZX297520V3_LPM_LTE_PCLK, "lpm-lte-pclk", clk_main[0], 0x58, 9, 0),
+ GATE(ZX297520V3_LPM_TD_WCLK, "lpm-td-wclk", clk_unknown, 0x58, 6, 0),
+ GATE(ZX297520V3_LPM_TD_PCLK, "lpm-td-pclk", clk_main[0], 0x58, 7, 0),
+ GATE(ZX297520V3_LPM_W_WCLK, "lpm-w-wclk", clk_unknown, 0x58, 4, 0),
+ GATE(ZX297520V3_LPM_W_PCLK, "lpm-w-pclk", clk_main[0], 0x58, 5, 0),
+
+ GATE(ZX297520V3_OUT0_WCLK, "out0-wclk", "out0-mux", 0x34, 0, 0),
+ GATE(ZX297520V3_OUT1_WCLK, "out1-wclk", "out1-mux", 0x90, 2, 0),
+ GATE(ZX297520V3_OUT2_WCLK, "out2-wclk", "out2-mux", 0x94, 2, 0),
+ GATE(ZX297520V3_OUT32K_WCLK, "out32k-wclk", "clock-32k", 0x34, 1, 0),
+ GATE(ZX297520V3_RMIIPHY_WCLK, "rmiiphy-wclk", "rmiiphy-mux", 0x11c, 2, 0),
+};
+
+static const struct zx_clk_data zx297520v3_topclk_data = {
+ .inputs = zx297520v3_top_inputs,
+ .num_inputs = ARRAY_SIZE(zx297520v3_top_inputs),
+ .plls = zx297520v3_plls,
+ .num_plls = ARRAY_SIZE(zx297520v3_plls),
+ .fixed_divs = zx297520v3_top_fixed_divs,
+ .num_fixed_divs = ARRAY_SIZE(zx297520v3_top_fixed_divs),
+ .muxes = zx297520v3_top_muxes,
+ .num_muxes = ARRAY_SIZE(zx297520v3_top_muxes),
+ .divs = zx297520v3_top_dividers,
+ .num_divs = ARRAY_SIZE(zx297520v3_top_dividers),
+ .gates = zx297520v3_top_gates,
+ .num_gates = ARRAY_SIZE(zx297520v3_top_gates),
+};
+
+static int clk_zx297520v3_probe(struct platform_device *pdev)
+{
+ const struct platform_device_id *id = platform_get_device_id(pdev);
+
+ if (!id)
+ return -ENODEV;
+
+ return zx_clk_common_probe(&pdev->dev, pdev->dev.parent->of_node,
+ (const struct zx_clk_data *)id->driver_data);
+}
+
+static const struct platform_device_id clk_zx297520v3_ids[] = {
+ {
+ .name = "zx297520v3-topclk",
+ .driver_data = (kernel_ulong_t)&zx297520v3_topclk_data,
+ },
+ { }
+};
+MODULE_DEVICE_TABLE(platform, clk_zx297520v3_ids);
+
+static struct platform_driver clk_zx297520v3 = {
+ .probe = clk_zx297520v3_probe,
+ .driver = {
+ .name = "clk-zx297520v3",
+ },
+ .id_table = clk_zx297520v3_ids,
+};
+module_platform_driver(clk_zx297520v3);
+
+MODULE_AUTHOR("Stefan Dösinger <stefandoesinger@gmail.com>");
+MODULE_DESCRIPTION("ZTE zx297520v3 clock driver");
+MODULE_LICENSE("GPL");
--
2.54.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* [PATCH v8 08/12] clk: zte: Introduce a driver for zx297520v3 top clocks
@ 2026-07-27 18:24 ` Stefan Dösinger
0 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
This register space controls core devices: PLLs, the AHB bus, a lot of
timers, the USB controller, the Cortex M0 processor that boots the board
and a few other devices. For some reason the LTE coprocessor is also
partially controlled by it. The main application processor and DDR
memory are not found here though.
The register to reboot the board is found here, as well as a register to
control whether watchdog expiries cause a board reset.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
Version 8:
Rename the expected clock name of the fixed oscillators to clock-26m and
clock-32k. Sashiko pointed out that "oscXXm" is deprecated, although
widespread in existing drivers.
Add a define indirection for the AXI candidate=critical protection.
While it holds true in a system with running rprocs, I find it useful
for stress testing the clock tree to disable it.
Double-checked pclk and wclk bits, fixed incorrect assignments in
register 0x54.
Version 7:
The register at top+0x140 contains a series of gates that control PLL
input into the top controller and PLL and osc26m work clock forward into
matrix. This allowed further insight into the clock tree.
The PLL subdivision list was replaced with fixed factor clocks that
divide PLLs before and after the input gate (and some fixed factor divs
go into matrix in the next patch). The number of clocks exported to
Matrix is actually relatively small.
As for Sashiko's suggestion about out of bound parents for dpll and
unknownpll: I have never seen a parent != 0. If that happens on a device
I want to know about it. At this stage of development I'd rather have
clearly visible orphaned clock than silently continuing at an unexpected
frequency.
"osc26m" -> "clock-26m" as "oscXX", although widespread, is deprecated.
In doing so, change underscores in clock names to dashes (Sashiko).
Version 5:
*) Make it a MFD child driver instead of binding to the node directly
*) Correct parents for dpll
*) Correct HSIC work clock parent
---
drivers/clk/zte/Kconfig | 11 +
drivers/clk/zte/Makefile | 1 +
drivers/clk/zte/clk-zx297520v3.c | 576 +++++++++++++++++++++++++++++++++++++++
3 files changed, 588 insertions(+)
diff --git a/drivers/clk/zte/Kconfig b/drivers/clk/zte/Kconfig
index 0222549dd211..4a4b26c94a9d 100644
--- a/drivers/clk/zte/Kconfig
+++ b/drivers/clk/zte/Kconfig
@@ -14,3 +14,14 @@ config COMMON_CLK_ZTE
of this.
Enable this if you are building a kernel for a ZTE designed board.
+
+config CLK_ZTE_ZX297520V3
+ tristate "Clock driver for ZTE zx297520v3 based SoCs"
+ depends on COMMON_CLK_ZTE && (MFD_ZTE_ZX297520V3_CRM || COMPILE_TEST)
+ default SOC_ZX297520V3
+ help
+ This driver supports ZTE zx297520v3 basic clocks.
+
+ Enable this if you want to build a kernel that is able to run on
+ boards based on this SoC. You can safely enable multiple clock
+ drivers. The one(s) matching the device tree will be used.
diff --git a/drivers/clk/zte/Makefile b/drivers/clk/zte/Makefile
index 27db07293165..2c073512e919 100644
--- a/drivers/clk/zte/Makefile
+++ b/drivers/clk/zte/Makefile
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_COMMON_CLK_ZTE) += clk-zte.o
+obj-$(CONFIG_CLK_ZTE_ZX297520V3) += clk-zx297520v3.o
clk-zte-y += clk-zx.o pll-zx.o clk-regmap.o
diff --git a/drivers/clk/zte/clk-zx297520v3.c b/drivers/clk/zte/clk-zx297520v3.c
new file mode 100644
index 000000000000..4444c2eb1382
--- /dev/null
+++ b/drivers/clk/zte/clk-zx297520v3.c
@@ -0,0 +1,576 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2026 Stefan Dösinger
+ *
+ * There are no public datasheets for zx297520v3. The controls on this clock controller have been
+ * extracted from ZTE's kernel and U-Boot sources and a LOT of manual testing. Some clocks can be
+ * found in the power management (evb_cpurpm.img) and LTE (cpko.ko) blobs shipped with the routers.
+ *
+ * Because ZTE's sources are wrong or misleading in some parts and testing can be misinterpreted,
+ * this file contains a LOT of comments on how particular clocks were tested and how the impact of
+ * changes to them can be observed.
+ */
+#include <dt-bindings/clock/zte,zx297520v3-clk.h>
+#include <linux/platform_device.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+#include <linux/types.h>
+#include <linux/errno.h>
+
+#include "clk-zx.h"
+
+MODULE_IMPORT_NS("ZTE_CLK");
+
+/* AXI speed is either determined by a fixed selection in matrix+0x0, bits 0:2 or by a voting system
+ * with ballots in matrix+0x120, 0x124, 0x128. The voting system is enabled by setting matrix 0x12c
+ * to 1. Otherwise the selection in 0x0 applies.
+ *
+ * The fastest selected speed wins. This holds true for values 0 (26 MHz) and 7 (6.5 MHz) too, so it
+ * isn't a case of lowest selector wins. I haven't tested what happens if mpll is changed to output
+ * 5x the normal clock, so 7 beats 0. Presumably value 0 would still win the election.
+ *
+ * AXI speed 6.5 is a child of matrix-osc26m, not a PLL. This can be seen by gating it off in
+ * topcrm+0x140.
+ *
+ * The system initializes with voting disabled and all preferences set to the lowest setting, 7.
+ * The zx297520v3_matrix_init function transfers the global mux to our vote and enables voting.
+ *
+ * Because rprocs may change their selection without notice, all possible parents of the AXI mux are
+ * marked critical. This can however obscure bugs in the clock tree. Define ZX297520V3_AXI_CANDIDATE
+ * to 0 to disable this protection for testing and either not run anything on the rprocs or make
+ * sure Linux votes for the fastest rate.
+ */
+#define ZX297520V3_AXI_CANDIDATE CLK_IS_CRITICAL
+
+/* Used for gates where we don't know the parent input(s). Assume general oscillator */
+static const char clk_unknown[] = "clock-26m";
+
+/* For clocks that use the ungated clock-26m input */
+static const char * const clk_main[] = {
+ "clock-26m",
+};
+
+static const char * const dpll_parents[] = {
+ "unknownpll-d2",
+ "clock-26m",
+};
+
+static const char * const zx297520v3_top_inputs[] = {
+ "osc26m",
+ "osc32k"
+};
+
+/* Top and matrix clocks are chaotic - I haven't found a consistent pattern behind their register
+ * and bit locations. Generally there are two gates (pclk, wclk), one mux, two resets and sometimes
+ * one divider, but exceptions apply. For some devices there is only a reset and some general
+ * (parent) clocks need setup. This structure plus macro handles the somewhat regular parts.
+ *
+ * There are some patterns that can be observed.
+ * mux 0x3c, div 0x48, gate 0x54
+ * mux 0x40, div 0x4c, gate 0x5c
+ * mux 0x44, div 0x50, gate 0x60
+ *
+ * For a 0 - 0xc - 0x18 pattern. Muxes from 0x3c to 0x44, dividers from 0x48 to 0x50, gates 0x54 to
+ * 0x60. The pattern is broken for timer t17 though.
+ *
+ * Gates have 4 bits per clock - usually, but not always, bit 0 for wclk, bit 1 for pclk, bit 2 for
+ * something the ZTE kernel calls "gate" (the bits we use here are called "en"), which presumably
+ * enables automatic clock gating. Bit 3, if settable, seems unused. E.g. offset 0x54 accepts all
+ * bits in 0xF77F7F7F - suggesting RTC, I2C0 have an extra gate bit.
+ *
+ * The default mpll settings multiply the 26 MHz reference clock times 24. A mux selection of 26 MHz
+ * could mean using the 26 MHz oscillator directly, or passing it through the PLL and divide by 24.
+ *
+ * If a UART is set to mpll-d6 (default 104 MHz), changing the mpll multipliers does affect UART
+ * timing as it should. This does not happen when the UART is set to 26 MHz input or timers that
+ * read 26 MHz input. This suggests 26 MHz clocks use the reference clock directly.
+ *
+ * The general clock flow on this board goes from clock-26m into the PLLs. The PLL output is
+ * branched into a small number of divided frequencies (mpll, mpll-d4, mpll-d5, mpll-d6, mpll-d8,
+ * gpll, gpll-d2, gpll-d4) at a high level. These divided frequencies as well as clock-26m go
+ * through a series of gates at offset 0x140 in topcrm. The output of these gates provide wclks for
+ * everything.
+ *
+ * At least 3 of those gate outputs (mpll, dpll, gpll-d2) are further divided behind those gates,
+ * presumably in the matrix controller. There are multiple paths how 104 and 78 MHz are derived,
+ * some specific to topcrm (0x140 bits 1 and 4), some specific to matrix (0x140 bit 9). There are
+ * likely 3 "copies" of the 78 MHz clock and two 50 MHz ones.
+ *
+ * Pclk is not gated by the 0x140 gates at all. Likewise clock-32k is fed to the timers without
+ * passing through this gate, even for consumers on matrix/LSP. Topcrm clock-26m wclks are not gated
+ * either, but top-consumed PLL clocks are.
+ */
+
+static const struct zx_fixed_divider_desc zx297520v3_top_fixed_divs[] = {
+ /* clock-26m division for clk_outX */
+ {"clock-26m-d2", "clock-26m", 2},
+
+ /* This one is a parent for dpll */
+ {"unknownpll-d2", "unknownpll", 2},
+
+ /* Pre-0x140 divisions. They enter topcrm through different bits in 0x140 */
+ {"mpll-d2", "mpll", 2},
+ {"mpll-d5", "mpll", 5},
+ {"mpll-d6", "mpll", 6},
+ {"mpll-d8", "mpll", 8},
+ {"upll-d12", "upll", 12},
+ {"gpll-d2", "gpll", 2},
+ {"gpll-d4", "gpll", 4},
+
+ /* Post-140 divisions. They enter through the same bit and can be selected by different mux
+ * values of their consumers or get piped to multiple consumers with different documented
+ * rates - in particular, USB wclk (24 MHz according to ZTE) shares bit 1 with M0's 78 MHz.
+ */
+ {"top-mpll-d2-d4", "top-mpll-d2", 4},
+ {"top-mpll-d2-d13", "top-mpll-d2", 13},
+ {"top-upll-d12-d2", "top-upll-d12", 2},
+ {"top-gpll-d4-d2", "top-gpll-d4", 2},
+};
+
+/* AHB: The clock mux works and impact can be tested e.g. with iperf speed testing of the USB
+ * network connection. Values 2 and 3 give the same speed and depend on the same bit (3) in 0x140.
+ * The mpll-d6 rate is gated off by the same bit in 0x140 as m0's mpll-d6, but the d8 is different.
+ * This may indicate that one of the two uses a different clock rate than documented in ZTE's
+ * kernel. The M0 speed is easy to measure, so I am confident in the M0's 78 MHz rate. AHB looks
+ * like 78 rather than 52, but confidence is lower because RAM read tests are influenced by CPU
+ * overhead too.
+ */
+static const char * const ahb_sel[] = {
+ "clock-26m",
+ "top-mpll-d6", /* 104 MHz */
+ "top-mpll-d8", /* 78 MHz */
+ "top-mpll-d8", /* 78 MHz */
+};
+
+static const char * const timer_top_sel[] = {
+ "clock-32k",
+ "clock-26m",
+};
+
+static const char * const uart_top_sel[] = {
+ "clock-26m",
+ "top-mpll-d6", /* 104 MHz, sharing M0's and ahb's 0x140 gate */
+};
+
+/* The Cortex M0 coprocessor. It is responsible for booting the board and runs some power management
+ * helper code on the stock firmware. The M0 rproc itself is not critical, but most of topcrm's
+ * registers become unreadable if this is gated off or become slow when clocked at 32 kHz, e.g. GPIO
+ * becomes borderline unusable at 32 kHz.
+ *
+ * There is no dedicated M0 gate. Disabling ahb-wclk looks like the M0 stops from A53's point of
+ * view, but it merely breaks the A53's ability to communicate with it, and M0's ability to
+ * communicate with the standard UART on LSP. M0 keeps running with both ahb-wclk and ahb-pclk
+ * gated off and can re-enable the gates.
+ */
+static const char * const m0_sel[] = {
+ "clock-26m",
+ "top-mpll-d6", /* 104 MHz */
+ "top-mpll-d2-d4", /* 78 MHz, but different gate than the ahb counterpart */
+ "clock-32k", /* Yes, tested. It is SLLLLOOOOOWWW. */
+};
+
+/* Clk-out0/1/2/32k: These clocks are exposed on GPIOs 15, 16, 17 and 18 respectively. They are used
+ * in ZTE's camera and sound code, by directly poking into the clock registers from the device
+ * drivers. Until the respective devices are supported they can safely be switched off.
+ *
+ * For clk-out1 ZTE's camera code says the following:
+ *
+ * 0 -> 20 MHz
+ * 1 -> 40 MHz
+ * 2 -> 13 MHz
+ * 3 -> 26 MHz
+ *
+ * 0 and 1 read from upll. I confirmed their rates (upll-d24 and upll-d12) by setting upll to a very
+ * low frequency and sampling the clock by GPIO reads. The outputs of 2 and 3 are way too high to
+ * test that way. Neither is reading from any PLL, they keep oscillating when all PLLs are disabled.
+ * I am fairly sure that 3 is just direct clock-26m because it perfectly matches what out2 is
+ * showing in its 26 MHz setting.
+ *
+ * Clk-out2 is similar, but it has only one control bit in top 0x34 bit 8. Neither setting selects
+ * a PLL output. When setting *0x34 = 0x080, clk-out1 and clk-out2 are in lockstep, presumably
+ * running at 13 MHz. 0x1c0 also runs in lockstep, presumably 26 MHz.
+ *
+ * clk-out0 has a mux in bit 5. Value 0 most likely selects clock-26m. I am not entirely sure about
+ * value 1 (which is the default), but the m0 mux has an impact on it. It looks like a debug pin
+ * that exposes some core clock.
+ */
+
+static const char * const out0_sel[] = {
+ "clock-26m",
+ "m0-mux",
+};
+
+static const char * const out1_sel[] = {
+ "top-upll-d12-d2", /* 20 MHz */
+ "top-upll-d12", /* 40 MHz */
+ "clock-26m-d2",
+ "clock-26m",
+};
+
+static const char * const out2_sel[] = {
+ "clock-26m-d2",
+ "clock-26m",
+};
+
+/* Clk-o is similar to clk-out*, providing a clock on GPIO 20, presumably for driving a (R)MII phy.
+ * The 50 MHz value is documented in a stray comment in ZTE's GMAC driver. I tested it similarly to
+ * the above pins. Mux setting 0 gives half the count as setting 1 and setting 1 gives gpll divided
+ * by 4 - matching the 50 MHz suggested by the ZTE comment at the default gpll settings. Gating gpll
+ * stops the clock for values 0 and 1.
+ *
+ * Settings 2 and 3 are possible, but seem to return garbage. It is generally pulsing up and down,
+ * except if both gpll and upll are stopped. I suspect it just reads random electrical fluctuation
+ * from other places in the board. Yes, I had a pull-down enabled when testing this.
+ *
+ * I am not aware of any board that uses this though. The Ethernet-equipped ones I have all run the
+ * PHY with its own oscillator.
+ */
+static const char * const rmii_sel[] = {
+ "top-gpll-d4-d2", /* 25 MHz */
+ "top-gpll-d4", /* 50 MHz */
+};
+
+static const struct zx_pll_desc zx297520v3_plls[] = {
+ /* Default setting: 0x48040c11. 624/312/156. Only a single possible parent. This is the
+ * PLL for pretty much everything, including CPU, RAM and USB.
+ *
+ * Changing this PLL makes it possible to overclock the CPU or do more fine grained
+ * underclocking than the CPU's mux allows. It does run into two problems though: The USB
+ * device uses this PLL's output directly and is *very* sensitive to differences. DRAM
+ * is also fed by this clock and needs to be re-trained on larger changes, which needs to
+ * be done by the stage 1 boot loader.
+ */
+ {
+ .name = "mpll",
+ .parents = clk_main,
+ .num_parents = ARRAY_SIZE(clk_main),
+ .rate = 0,
+ .reg = 0x8
+ },
+
+ /* ZTE's code calls this PLL "upll". The only possible consumers I found are clk-out1,
+ * which outputs this clock on GPIO 16 and HSIC. HSIC doesn't have anything connected to
+ * it on the devices I tested. The device that consumes clk-out1 seems to be an SPI
+ * camera, which I haven't seen in any device so far. ZTE manipulates this PLL directly out
+ * of the camera code, so it is unlikely to have another consumer.
+ *
+ * Long story short, shut it off.
+ */
+ {
+ .name = "upll",
+ .parents = clk_main,
+ .num_parents = ARRAY_SIZE(clk_main),
+ .rate = 0,
+ .reg = 0x10,
+ },
+
+ /* Default value 0x4834902d. Feeds dpll. 46.08 MHz. Bit 25 can be set, so two parents are
+ * possible. It looks like both values select the 26 MHz oscillator though.
+ *
+ * Since dpll's prepare may depend on this clock generating a usable signal, it needs to be
+ * enabled when prepared.
+ */
+ {
+ .name = "unknownpll",
+ .parents = clk_main,
+ .num_parents = ARRAY_SIZE(clk_main),
+ .rate = 0,
+ .reg = 0x100,
+ .flags = ZX297520V3_PLL_PREPARE_IS_ENABLE,
+ },
+
+ /* The documentation says 491.52 MHz and measurement with the LSP TDM device supports this.
+ * The default value is 0x480c2011, but not all boot loaders set it up. To get to 491.52
+ * with these settings it needs a 23.04 MHz reference clock, which matches unknownpll-d2.
+ * If unknownpll is disabled, dpll loses its lock. We set the frequency on this PLL if we
+ * find it is not enabled by the boot loader.
+ *
+ * The proprietary LTE driver or rproc enables and disables it. TDM and I2S can use it.
+ *
+ * It accepts parent values 0, 1, 2 and 3. Parent 0 is unknownpll-d2. The others look like
+ * clock-26m. With a parent != 0 dpll never loses its lock even when all other PLLs are off
+ * and the TDM counter register increases at a rate consistent with a 26.0/23.04 clock
+ * increase.
+ */
+ {
+ .name = "dpll",
+ .parents = dpll_parents,
+ .num_parents = ARRAY_SIZE(dpll_parents),
+ .rate = 491520000,
+ .reg = 0x18,
+ },
+
+ /* "g" is either for "general" or "gigahertz". The VCO runs at 1 GHz. Output clocks are 200,
+ * 100, 50, 25, ... MHz. It is used optionally by SDIO and QSPI and can drive a GPIO clock
+ * output for RMII, so it doesn't seem very general.
+ */
+ {
+ .name = "gpll",
+ .parents = clk_main,
+ .num_parents = ARRAY_SIZE(clk_main),
+ .rate = 0,
+ .reg = 0x110,
+ },
+};
+
+#define MUX(_id, _name, _parents, _reg, _shift, _size) { \
+ _id, _name, _parents, ARRAY_SIZE(_parents), _reg, _shift, _size}
+
+#define DIV(_name, _parent, _reg, _shift, _size) { _name, _parent, _reg, _shift, _size }
+
+#define GATE(_id, _name, _parent, _reg, _shift, _flags) { \
+ .id = _id, \
+ .name = _name, \
+ .parent = _parent, \
+ .flags = _flags, \
+ .reg = _reg, \
+ .shift = _shift, \
+}
+
+static const struct zx_mux_desc zx297520v3_top_muxes[] = {
+ MUX(ZX297520V3_M0_WCLK, "m0-mux", m0_sel, 0x38, 0, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "ahb-mux", ahb_sel, 0x3c, 4, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-t08-mux", timer_top_sel, 0x40, 1, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-t09-mux", timer_top_sel, 0x40, 0, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-t12-mux", timer_top_sel, 0x3c, 0, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-t13-mux", timer_top_sel, 0x44, 0, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-t14-mux", timer_top_sel, 0x44, 1, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-t15-mux", timer_top_sel, 0x3c, 3, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-t16-mux", timer_top_sel, 0x44, 2, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-t17-mux", timer_top_sel, 0x120, 0, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "wdt-t18-mux", timer_top_sel, 0x3c, 6, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "i2c0-mux", uart_top_sel, 0x3c, 1, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "uart0-mux", uart_top_sel, 0x40, 2, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "out0-mux", out0_sel, 0x34, 5, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "out1-mux", out1_sel, 0x34, 6, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "out2-mux", out2_sel, 0x34, 8, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "rmiiphy-mux", rmii_sel, 0x11c, 0, 2),
+};
+
+static const struct zx_div_desc zx297520v3_top_dividers[] = {
+ DIV("timer-t08-div", "timer-t08-mux", 0x4c, 8, 4),
+ DIV("timer-t09-div", "timer-t09-mux", 0x4c, 0, 4),
+ DIV("timer-t12-div", "timer-t12-mux", 0x48, 0, 4),
+ DIV("timer-t13-div", "timer-t13-mux", 0x50, 0, 4),
+ DIV("timer-t14-div", "timer-t14-mux", 0x50, 4, 4),
+ DIV("timer-t15-div", "timer-t15-mux", 0x48, 4, 4),
+ DIV("timer-t16-div", "timer-t16-mux", 0x50, 8, 4),
+ DIV("timer-t17-div", "timer-t17-mux", 0x124, 0, 4),
+ DIV("wdt-t18-div", "wdt-t18-mux", 0x48, 8, 4),
+ DIV("usim1-div", clk_unknown, 0x48, 12, 1),
+};
+
+static const struct zx_gate_desc zx297520v3_top_gates[] = {
+ /* topcrm input gates. These are complicated, but shed a lot of light on the board's clock
+ * distribution.
+ *
+ * Settable mask: 0x017333FF. Generally one nibble per PLL, except for MPLL that has more.
+ * One for clock-26m work clock. pclk and clock-32k are not gated globally here or anywhere
+ * else as far as I can see.
+ *
+ * If you want to test your understanding of this board, try to manually configure it into a
+ * setup where bit 24 or bit 9 are off without crashing it.
+ */
+
+ /* Bit 0: No known consumer */
+ /* Bit 1: M0's 78 MHz selection, but somehow also involved in USB too */
+ GATE(ZX297520V3_CLK_NO_EXPORT, "top-mpll-d2", "mpll-d2", 0x140, 1, 0),
+ /* Bit 2: No known consumer, named "mpll_48m_clk" in ZTE's cpko.ko blob */
+ GATE(ZX297520V3_CLK_NO_EXPORT, "top-mpll-d8", "mpll-d8", 0x140, 3, 0),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "top-mpll-d6", "mpll-d6", 0x140, 4, 0),
+ /* Bit 5: No known consumer. "mpll_156m_clk" in cpko.ko */
+ GATE(ZX297520V3_MPLL_D5, "matrix-mpll-d5", "mpll-d5", 0x140, 6,
+ ZX297520V3_AXI_CANDIDATE),
+ /* Bit 7: No known consumer */
+ /* Bit 8: Has a consumer, LTE depends on it. But unknown what and how. Most likely one of
+ * the gsm clocks in topcrm+0x64 listed in cpko.ko.
+ */
+ /* Bit 9: Big MPLL provider for Matrix. It is an AXI voting candidate, but it is protected
+ * in Matrix's 0x118 gates.
+ */
+ GATE(ZX297520V3_MPLL, "matrix-mpll", "mpll", 0x140, 9, 0),
+ /* Bit 10: Always 0 */
+ /* Bit 11: Always 0 */
+ /* Bit 12: No known consumer. Existing patterns indicate a dpll consumer in top itself */
+ GATE(ZX297520V3_DPLL, "matrix-dpll", "dpll", 0x140, 13, 0),
+ /* Bit 14: Always 0 */
+ /* Bit 15: Always 0 */
+ GATE(ZX297520V3_CLK_NO_EXPORT, "top-upll-d12", "upll-d12", 0x140, 16, 0),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "top-upll", "upll", 0x140, 17, 0),
+ /* Bit 18: Always 0 */
+ /* Bit 19: Always 0 */
+ GATE(ZX297520V3_CLK_NO_EXPORT, "top-gpll-d4", "gpll-d4", 0x140, 20, 0),
+ GATE(ZX297520V3_GPLL_D2, "matrix-gpll-d2", "gpll-d2", 0x140, 21, 0),
+ GATE(ZX297520V3_GPLL, "matrix-gpll", "gpll", 0x140, 22, 0),
+ /* Bit 23: Always 0 */
+ /* Bit 24 covers all 26 MHz on matrix, but not pclks. Clock-26m consumers on topcrm do not
+ * use this, they get the ungated clock-26m. It is an AXI voting candidate, but does not
+ * need protection because it is the slowest speed. It will only be used if every processor,
+ * including us, votes for it.
+ */
+ GATE(ZX297520V3_GATED_OSC26M, "matrix-osc26m", "clock-26m", 0x140, 24, 0),
+
+ /* AHB clock gates: Disabling these cuts off the A53 from register access into 0x130000/
+ * 0x140000, including the topcrm region, so it can't be re-enabled. Mark them critical for
+ * that reason.
+ *
+ * Gating this off does not stop the M0 rproc, nor does it stop M0 from accessing topcrm. It
+ * does however gate off M0 from the AXI interconnect, so it can't read RAM, Matrix, etc. So
+ * these bits here merely gate the bus, not the actual devices.
+ */
+ GATE(ZX297520V3_AHB_PCLK, "ahb-pclk", "ahb-mux", 0x54, 12,
+ CLK_IS_CRITICAL),
+ GATE(ZX297520V3_AHB_WCLK, "ahb-wclk", "m0-mux", 0x54, 13,
+ CLK_IS_CRITICAL),
+
+ /* SRAM1 and 2 clocks. Leave them on for now, as turning them off carelessly hangs the M0 */
+ GATE(ZX297520V3_SRAM1_PCLK, "sram1-pclk", clk_main[0], 0x54, 18,
+ CLK_IS_CRITICAL),
+ GATE(ZX297520V3_SRAM2_PCLK, "sram2-pclk", clk_main[0], 0x54, 16,
+ CLK_IS_CRITICAL),
+
+ /* Pinmux (AON, TOP, IOCFG but not PDCFG). Critical as well until we have a driver that
+ * consumes it. I don't think we'll realistically shut this off ever.
+ *
+ * Setting either bit 0 or 1 in register 0x58 makes the device work.
+ */
+ GATE(ZX297520V3_PMM_WCLK, "pmm-wclk", clk_main[0], 0x58, 0,
+ CLK_IS_CRITICAL),
+ GATE(ZX297520V3_PMM_PCLK, "pmm-pclk", clk_main[0], 0x58, 1,
+ CLK_IS_CRITICAL),
+
+ /* Timers. We don't use any of them, just shut them off. The timers are named and sorted
+ * by the IO address of the main timer controls. Some of the controls are documented in
+ * ZTE's kernel, the others I found by trial and error. Timers in register 0x54 have the
+ * pclk first, the others have the wclk first.
+ *
+ * Timer T17 is used by the ZSP firmware. The rproc driver will enable it as needed.
+ */
+ GATE(ZX297520V3_TIMER_T08_WCLK, "timer-t08-wclk", "timer-t08-div", 0x5c, 8, 0),
+ GATE(ZX297520V3_TIMER_T08_PCLK, "timer-t08-pclk", clk_main[0], 0x5c, 9, 0),
+ GATE(ZX297520V3_TIMER_T09_WCLK, "timer-t09-wclk", "timer-t09-div", 0x5c, 4, 0),
+ GATE(ZX297520V3_TIMER_T09_PCLK, "timer-t09-pclk", clk_main[0], 0x5c, 5, 0),
+ GATE(ZX297520V3_TIMER_T12_WCLK, "timer-t12-wclk", "timer-t12-div", 0x54, 5, 0),
+ GATE(ZX297520V3_TIMER_T12_PCLK, "timer-t12-pclk", clk_main[0], 0x54, 4, 0),
+ GATE(ZX297520V3_TIMER_T13_WCLK, "timer-t13-wclk", "timer-t13-div", 0x60, 0, 0),
+ GATE(ZX297520V3_TIMER_T13_PCLK, "timer-t13-pclk", clk_main[0], 0x60, 1, 0),
+ GATE(ZX297520V3_TIMER_T14_WCLK, "timer-t14-wclk", "timer-t14-div", 0x60, 4, 0),
+ GATE(ZX297520V3_TIMER_T14_PCLK, "timer-t14-pclk", clk_main[0], 0x60, 5, 0),
+ GATE(ZX297520V3_TIMER_T15_WCLK, "timer-t15-wclk", "timer-t15-div", 0x54, 21, 0),
+ GATE(ZX297520V3_TIMER_T15_PCLK, "timer-t15-pclk", clk_main[0], 0x54, 20, 0),
+ GATE(ZX297520V3_TIMER_T16_WCLK, "timer-t16-wclk", "timer-t16-div", 0x60, 8, 0),
+ GATE(ZX297520V3_TIMER_T16_PCLK, "timer-t16-pclk", clk_main[0], 0x60, 9, 0),
+ GATE(ZX297520V3_TIMER_T17_WCLK, "timer-t17-wclk", "timer-t17-div", 0x128, 0, 0),
+ GATE(ZX297520V3_TIMER_T17_PCLK, "timer-t17-pclk", clk_main[0], 0x128, 1, 0),
+ /* This watchdog is set up by the bootloader and in normal operation the m0 firmware will
+ * feed the dog. The m0 firmware in turn wants to be fed in its own way. Since we normally
+ * don't run any m0 firmware we shut it off by default and expose it to userspace via the
+ * watchdog driver.
+ */
+ GATE(ZX297520V3_WDT_T18_WCLK, "wdt-t18-wclk", "wdt-t18-div", 0x54, 25, 0),
+ GATE(ZX297520V3_WDT_T18_PCLK, "wdt-t18-pclk", clk_main[0], 0x54, 24, 0),
+
+ GATE(ZX297520V3_I2C0_WCLK, "i2c0-wclk", "i2c0-mux", 0x54, 9, 0),
+ GATE(ZX297520V3_I2C0_PCLK, "i2c0-pclk", clk_main[0], 0x54, 8, 0),
+ GATE(ZX297520V3_UART0_WCLK, "uart0-wclk", "uart0-mux", 0x5c, 12, 0),
+ GATE(ZX297520V3_UART0_PCLK, "uart0-pclk", clk_main[0], 0x5c, 13, 0),
+
+ /* ZTE says the USB input is a 24 MHz clock based on mpll. Testing shows that Upll is not
+ * involved. The USB register space can be accessed with ahb-pclk gated, but not with
+ * ahb-wclk gated. ZTE also lists ahb-wclk as parent for the second clock.
+ *
+ * There doesn't seem to be a separate PHY clock. usb-wclk stops controller registers from
+ * updating, but doesn't cause the USB device to disconnect like asserting the PHY reset
+ * does. Bit 0 in this register can't be set, so no PHY clock is hiding there either.
+ */
+ GATE(ZX297520V3_USB_WCLK, "usb-wclk", "top-mpll-d2-d13", 0x6c, 3, 0),
+ GATE(ZX297520V3_USB_PCLK, "usb-pclk", "ahb-wclk", 0x6c, 4, 0),
+ /* The HSIC hardware is listed in ZTE's code with a physical address of 0x01600000. Unlike
+ * the USB controller, it does consume upll. ZTE says 240 MHz, but 480 is the usual one for
+ * HSIC.
+ */
+ GATE(ZX297520V3_HSIC_WCLK, "hsic-wclk", "top-upll", 0x6c, 1, 0),
+ GATE(ZX297520V3_HSIC_PCLK, "hsic-pclk", "ahb-wclk", 0x6c, 2, 0),
+
+ /* How does this RTC work? I don't know, the ZTE kernel does not talk to it. The actual RTC
+ * is on the I2C-connected PMIC.
+ */
+ GATE(ZX297520V3_RTC_WCLK, "rtc-wclk", clk_unknown, 0x54, 1, 0),
+ GATE(ZX297520V3_RTC_PCLK, "rtc-pclk", clk_main[0], 0x54, 0, 0),
+
+ GATE(ZX297520V3_USIM1_WCLK, "usim1-wclk", "usim1-div", 0x54, 29, 0),
+ GATE(ZX297520V3_USIM1_PCLK, "usim1-pclk", clk_main[0], 0x54, 28, 0),
+
+ /* LTE: gate only as far as I can see. I looked for resets and did not find any. There may
+ * be mux/div, but without understanding the behavior of this hardware it is impossible to
+ * tell. They are sorted by physical MMIO address of the devices, which happens to be the
+ * inverse order of the bits.
+ *
+ * I don't know what "LPM", "TD" and "W" mean. I copied them from ZTE's names.
+ *
+ * Like PMM in this register, either bit enables register access, whether they control pclk
+ * or wclk cannot be distinguished.
+ */
+ GATE(ZX297520V3_LPM_GSM_WCLK, "lpm-gsm-wclk", clk_unknown, 0x58, 10, 0),
+ GATE(ZX297520V3_LPM_GSM_PCLK, "lpm-gsm-pclk", clk_main[0], 0x58, 11, 0),
+ GATE(ZX297520V3_LPM_LTE_WCLK, "lpm-lte-wclk", clk_unknown, 0x58, 8, 0),
+ GATE(ZX297520V3_LPM_LTE_PCLK, "lpm-lte-pclk", clk_main[0], 0x58, 9, 0),
+ GATE(ZX297520V3_LPM_TD_WCLK, "lpm-td-wclk", clk_unknown, 0x58, 6, 0),
+ GATE(ZX297520V3_LPM_TD_PCLK, "lpm-td-pclk", clk_main[0], 0x58, 7, 0),
+ GATE(ZX297520V3_LPM_W_WCLK, "lpm-w-wclk", clk_unknown, 0x58, 4, 0),
+ GATE(ZX297520V3_LPM_W_PCLK, "lpm-w-pclk", clk_main[0], 0x58, 5, 0),
+
+ GATE(ZX297520V3_OUT0_WCLK, "out0-wclk", "out0-mux", 0x34, 0, 0),
+ GATE(ZX297520V3_OUT1_WCLK, "out1-wclk", "out1-mux", 0x90, 2, 0),
+ GATE(ZX297520V3_OUT2_WCLK, "out2-wclk", "out2-mux", 0x94, 2, 0),
+ GATE(ZX297520V3_OUT32K_WCLK, "out32k-wclk", "clock-32k", 0x34, 1, 0),
+ GATE(ZX297520V3_RMIIPHY_WCLK, "rmiiphy-wclk", "rmiiphy-mux", 0x11c, 2, 0),
+};
+
+static const struct zx_clk_data zx297520v3_topclk_data = {
+ .inputs = zx297520v3_top_inputs,
+ .num_inputs = ARRAY_SIZE(zx297520v3_top_inputs),
+ .plls = zx297520v3_plls,
+ .num_plls = ARRAY_SIZE(zx297520v3_plls),
+ .fixed_divs = zx297520v3_top_fixed_divs,
+ .num_fixed_divs = ARRAY_SIZE(zx297520v3_top_fixed_divs),
+ .muxes = zx297520v3_top_muxes,
+ .num_muxes = ARRAY_SIZE(zx297520v3_top_muxes),
+ .divs = zx297520v3_top_dividers,
+ .num_divs = ARRAY_SIZE(zx297520v3_top_dividers),
+ .gates = zx297520v3_top_gates,
+ .num_gates = ARRAY_SIZE(zx297520v3_top_gates),
+};
+
+static int clk_zx297520v3_probe(struct platform_device *pdev)
+{
+ const struct platform_device_id *id = platform_get_device_id(pdev);
+
+ if (!id)
+ return -ENODEV;
+
+ return zx_clk_common_probe(&pdev->dev, pdev->dev.parent->of_node,
+ (const struct zx_clk_data *)id->driver_data);
+}
+
+static const struct platform_device_id clk_zx297520v3_ids[] = {
+ {
+ .name = "zx297520v3-topclk",
+ .driver_data = (kernel_ulong_t)&zx297520v3_topclk_data,
+ },
+ { }
+};
+MODULE_DEVICE_TABLE(platform, clk_zx297520v3_ids);
+
+static struct platform_driver clk_zx297520v3 = {
+ .probe = clk_zx297520v3_probe,
+ .driver = {
+ .name = "clk-zx297520v3",
+ },
+ .id_table = clk_zx297520v3_ids,
+};
+module_platform_driver(clk_zx297520v3);
+
+MODULE_AUTHOR("Stefan Dösinger <stefandoesinger@gmail.com>");
+MODULE_DESCRIPTION("ZTE zx297520v3 clock driver");
+MODULE_LICENSE("GPL");
--
2.54.0
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 50+ messages in thread* Re: [PATCH v8 08/12] clk: zte: Introduce a driver for zx297520v3 top clocks
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 19:29 ` sashiko-bot
-1 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 19:29 UTC (permalink / raw)
To: Stefan Dösinger
Cc: lee, devicetree, mfd, vkoul, conor+dt, robh, neil.armstrong,
olteanv, linux-phy
> This register space controls core devices: PLLs, the AHB bus, a lot of
> timers, the USB controller, the Cortex M0 processor that boots the board
> and a few other devices. For some reason the LTE coprocessor is also
> partially controlled by it. The main application processor and DDR
> memory are not found here though.
>
> The register to reboot the board is found here, as well as a register to
> control whether watchdog expiries cause a board reset.
>
> Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=8
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 50+ messages in thread
* Re: [PATCH v8 08/12] clk: zte: Introduce a driver for zx297520v3 top clocks
@ 2026-07-27 19:29 ` sashiko-bot
0 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 19:29 UTC (permalink / raw)
To: Stefan Dösinger
Cc: lee, devicetree, mfd, vkoul, conor+dt, robh, neil.armstrong,
olteanv, linux-phy
> This register space controls core devices: PLLs, the AHB bus, a lot of
> timers, the USB controller, the Cortex M0 processor that boots the board
> and a few other devices. For some reason the LTE coprocessor is also
> partially controlled by it. The main application processor and DDR
> memory are not found here though.
>
> The register to reboot the board is found here, as well as a register to
> control whether watchdog expiries cause a board reset.
>
> Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=8
^ permalink raw reply [flat|nested] 50+ messages in thread
* [PATCH v8 09/12] clk: zte: Introduce a driver for zx297520v3 matrix clocks
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 18:24 ` Stefan Dösinger
-1 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
This clock controller controls high speed devices: CPU, DMA, RAM, SDIO,
Ethernet. The only non-clock, non-reset registers I have spotted here
are hardware spinlocks.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
Version 8:
Add another distribution gate register: 0x118 gates off matrix-generated
divisions of mpll, dpll and gpll. It nicely fits the top->matrix forward
picture.
AXI 6.5 MHz is not an mpll child. I must have done something wrong when
originally testing this
Add explanations about additional ARM architected timer controls.
Add gates for unknown LTE hardware at offset 0x60. They are not exported
in the bindings because the actual name of the consumer isn't clear -
but if dpll is unprepared and feeds a bypass 26 MHz signal, this device
starts spamming IRQs (although the 0x118 and top+0x140 gates also
prevent this usually)
Double-check pclk and wclk of all devices. The SDIO and EDCP clocks were
flipped in previous submissions.
Version 7:
Reduced the input clocks a lot. Most MPLL subdivisions are children of
the same gate in top, so put the fixed factor clocks here.
Added a few more clocks: DDR_CTRL, AXI bus, ARM architected timer.
They are needed to tell CCF to keep the top gates enabled and marked
critical because they aren't consumed by a DT node.
The AXI bus is determined by a voting system. I've set up the driver to
write to the ballot register ZTE's kernel uses and enable voting. This
should make life easier with the LTE rproc blob later.
Version 5:
Make it an MFD child device, remove aux device
Fix zx297529 -> zx297520 typos
Fix AHB_wclk -> ahb_wclk. That was a leftover of my old composite structs
Add SRAM0 and GSM_CFG clocks
---
drivers/clk/zte/clk-zx297520v3.c | 338 +++++++++++++++++++++++++++++++++++++++
1 file changed, 338 insertions(+)
diff --git a/drivers/clk/zte/clk-zx297520v3.c b/drivers/clk/zte/clk-zx297520v3.c
index 4444c2eb1382..9299449097b4 100644
--- a/drivers/clk/zte/clk-zx297520v3.c
+++ b/drivers/clk/zte/clk-zx297520v3.c
@@ -542,6 +542,340 @@ static const struct zx_clk_data zx297520v3_topclk_data = {
.num_gates = ARRAY_SIZE(zx297520v3_top_gates),
};
+/* For devices which have a working driver the work clock can be figured out by gating off registers
+ * in top+0x140. This is used for devices where I can't interpret the register contents yet.
+ */
+static const char matrix_unk[] = "matrix-osc26m";
+
+static const char * const cpu_sel[] = {
+ "matrix-osc26m",
+ "matrix-mpll", /* 624 MHz */
+ "matrix-mpll-d2", /* 312 MHz */
+ "matrix-mpll-d4", /* 156 MHz */
+};
+
+/* Low confidence on the actual value, but at least I know it is bit 13 in top+0x140 */
+static const char * const zsp_sel[] = {
+ "matrix-osc26m",
+ "matrix-dpll", /* 491.52 MHz */
+};
+
+/* We can't realistically change DDR speed while running an OS out of DDR (it reads garbage for a
+ * short while on transition), but we need to know if we have to keep gpll alive. 32 MB devices use
+ * 200 MHz, while 64/128 MB ones use 156 MHz.
+ */
+static const char * const ddr_ctrl_sel[] = {
+ "matrix-mpll-d4", /* 156 MHz */
+ "matrix-gpll", /* 200 MHz */
+ "matrix-mpll-d6", /* 104 MHz */
+ "matrix-mpll-d8", /* 78 MHz */
+};
+
+static const char * const sd0_sel[] = {
+ "matrix-osc26m",
+ "matrix-mpll-d4", /* 156 MHz */
+ "matrix-gpll-d2", /* 100 MHz */
+ "matrix-mpll-d8", /* 78 MHz */
+ "matrix-gpll-d2-d2", /* 50 MHz */
+ "matrix-gpll-d2-d4", /* 25 MHz */
+};
+
+static const char * const sd1_sel[] = {
+ "matrix-osc26m",
+ "matrix-gpll-d2", /* 100 MHz */
+ "matrix-mpll-d8", /* 78 MHz */
+ "matrix-gpll-d2-d2", /* 50 MHz */
+ "matrix-mpll-d16", /* 39 MHz */
+ "matrix-gpll-d2-d4", /* 25 MHz */
+};
+
+/* ZTE's kernel puts the high frequency first, but this is against the usual convention on this
+ * SoC. I don't have any device that has a raw NAND controller though.
+ */
+static const char * const nand_sel[] = {
+ "matrix-osc26m",
+ "matrix-mpll-d4", /* 156 MHz */
+};
+
+static const char * const edcp_sel[] = {
+ "matrix-osc26m",
+ "matrix-mpll-d4", /* 156 MHz */
+ "matrix-mpll-d5", /* 124.8 MHz */
+ "matrix-mpll-d6", /* 104 MHz */
+};
+
+static const char * const tdm_sel[] = {
+ "matrix-osc26m",
+ "matrix-dpll-d4", /* 122.88 MHz */
+ "matrix-mpll-d6", /* 104 MHz */
+};
+
+/* ZTE's firmware uses 0x128 as the PHY's vote and names 0x120 as "PS" and 0x124 as "AP" vote.
+ * However, their cpufreq driver ultimately uses the "PS" define and thus writes to 0x120, which can
+ * be observed in the running system.
+ */
+#define ZX297520V3_AXI_AP_VOTE 0x120
+static const char * const axi_sel[] = {
+ "matrix-osc26m",
+ "matrix-mpll-d4", /* 156 MHz */
+ "matrix-mpll-d5", /* 124.8 MHz */
+ "matrix-mpll-d6", /* 104 MHz */
+ "matrix-mpll-d8", /* 78 MHz */
+ "matrix-mpll-d12", /* 52 MHz */
+ "matrix-mpll-d16", /* 39 MHz */
+ "matrix-osc26m-d4", /* 6.5 MHz */
+};
+
+static const struct zx_mux_desc zx297520v3_matrix_muxes[] = {
+ MUX(ZX297520V3_CLK_NO_EXPORT, "cpu-mux", cpu_sel, 0x20, 0, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "zsp-mux", zsp_sel, 0x30, 0, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "ddr-ctrl-mux", ddr_ctrl_sel, 0x50, 0, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "sd0-mux", sd0_sel, 0x50, 4, 3),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "sd1-mux", sd1_sel, 0x50, 8, 3),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "nand-mux", nand_sel, 0x50, 12, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "edcp-mux", edcp_sel, 0x50, 16, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "tdm-mux", tdm_sel, 0x50, 24, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "axi-mux", axi_sel, ZX297520V3_AXI_AP_VOTE, 0, 3),
+};
+
+static const struct zx_gate_desc zx297520v3_matrix_gates[] = {
+ /* Matrix subdivision gates: Almost every matrix-generated subdivision of the input PLLs
+ * has a gate in 0x118. The original inputs from topcrm (mpll 624, mpll 124.8, gpll 200,
+ * gpll 100, dpll 491.52) do not.
+ *
+ * I do not know of any user of dpll-d6 and do not know the divisor. An 81.92 MHz clock is
+ * listed as a root clock without a consumer in ZTE's code. Most likely it is a possible
+ * parent of the ZSP mux.
+ */
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-mpll-d16", "matrix-mpll-d16-pregate",
+ 0x118, 0, ZX297520V3_AXI_CANDIDATE),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-mpll-d12", "matrix-mpll-d12-pregate",
+ 0x118, 1, ZX297520V3_AXI_CANDIDATE),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-mpll-d8", "matrix-mpll-d8-pregate",
+ 0x118, 2, ZX297520V3_AXI_CANDIDATE),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-mpll-d6", "matrix-mpll-d6-pregate",
+ 0x118, 3, ZX297520V3_AXI_CANDIDATE),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-mpll-d4", "matrix-mpll-d4-pregate",
+ 0x118, 4, ZX297520V3_AXI_CANDIDATE),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-mpll-d2", "matrix-mpll-d2-pregate",
+ 0x118, 5, 0),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-dpll-d6", "matrix-dpll-d6-pregate",
+ 0x118, 8, 0),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-dpll-d4", "matrix-dpll-d4-pregate",
+ 0x118, 9, 0),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-gpll-d2-d4", "matrix-gpll-d2-d4-pregate",
+ 0x118, 12, 0),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-gpll-d2-d2", "matrix-gpll-d2-d2-pregate",
+ 0x118, 13, 0),
+
+ /* This bit cuts off the clock signal to the ARM architected timer, which the kernel uses
+ * as its main timer. It isn't critical per se - there are plenty of proprietary timers
+ * available that could be used - but the arm arch timer binding does not accept a clock, so
+ * this CCF driver won't know if the timer is in use.
+ *
+ * This clock is fed by the mux in topclk+0x140 - this critical clock here protects its
+ * parent.
+ *
+ * Registers 0x148, 0x14c, 0x150, 0x154 allow setting and clearing the timer counter. To
+ * set the timer, load a value into 0x148 (high 32 bits) and 0x14c (low 32 bits), then
+ * toggle 0x150 to 1. 0x150 will automatically reset to 0. 0x154 sets the timer to some
+ * "clear" value, but this value isn't 0.
+ */
+ GATE(ZX297520V3_SYS_TIMER_WCLK, "sys-timer-wclk", "matrix-osc26m", 0x144, 1,
+ CLK_IS_CRITICAL),
+
+ /* Both 0x24 and 0x28 bits 1 and 2 stop the CPU. There is also a bit in topclk+0x138, which
+ * ZTE's uboot calls "A53 reset", which also stops the CPU. I can't really tell the
+ * difference between matrix+0x28 and top+0x138. The clock (matrix+0x24) can be disabled
+ * and enabled from the Cortex M0 and it will nicely stop and restart the A53, retaining
+ * all state.
+ */
+ GATE(ZX297520V3_CPU_WCLK, "cpu-wclk", "cpu-mux", 0x24, 1,
+ CLK_IS_CRITICAL),
+ GATE(ZX297520V3_CPU_PCLK, "cpu-pclk", clk_main[0], 0x24, 2,
+ CLK_IS_CRITICAL),
+
+ /* There are a lot more controls in matrix+0x100. 13-16 appear to be 4 different AXI
+ * channels for different priorities. Bit 19 appears to be the DDR PHY wclk.
+ *
+ * The important task is to keep gpll enabled if the bootloader selected a gpll-based rate
+ * for RAM.
+ */
+ GATE(ZX297520V3_DDR_CTRL_PCLK, "ddr-ctrl-pclk", clk_main[0], 0x100, 17,
+ CLK_IS_CRITICAL),
+ GATE(ZX297520V3_DDR_CTRL_WCLK, "ddr-ctrl-wclk", "ddr-ctrl-mux", 0x100, 18,
+ CLK_IS_CRITICAL),
+
+ GATE(ZX297520V3_ZSP_WCLK, "zsp-wclk", "zsp-mux", 0x3c, 0, 0),
+
+ GATE(ZX297520V3_SD0_PCLK, "sd0-pclk", clk_main[0], 0x54, 12, 0),
+ GATE(ZX297520V3_SD0_WCLK, "sd0-wclk", "sd0-mux", 0x54, 13, 0),
+ GATE(ZX297520V3_SD0_CDET, "sd0-cdet", "clock-32k", 0x54, 14, 0),
+ GATE(ZX297520V3_SD1_PCLK, "sd1-pclk", clk_main[0], 0x54, 4, 0),
+ GATE(ZX297520V3_SD1_WCLK, "sd1-wclk", "sd1-mux", 0x54, 5, 0),
+ /* I don't know how the cdet clock works. Card detection in the way the dwc,mmc driver uses
+ * it appears broken no matter this clock's setting.
+ */
+ GATE(ZX297520V3_SD1_CDET, "sd1-cdet", "clock-32k", 0x54, 6, 0),
+
+ /* This is some "denali" NAND, not the qspi connected one */
+ GATE(ZX297520V3_NAND_WCLK, "nand-wclk", "nand-mux", 0x54, 20, 0),
+ GATE(ZX297520V3_NAND_PCLK, "nand-pclk", clk_main[0], 0x54, 21, 0),
+
+ /* There is a set of gates for an unknown device at matrix+0x60. It is some LTE related
+ * device. If it is fed an incorrect clock, e.g. because dpll is not locked and outputs
+ * 26 MHz, it will spam IRQs at SPI 68. This is likely a distress notification that some
+ * internal PLL did not lock or similar
+ *
+ * ZTE's kernel has two defines for SPI 64: "LTE_TPU_INT_PS_INT" and "VOU_OSD_INT". The
+ * latter is unlikely because VOU related clocks sit in matrix+0x168.
+ *
+ * Bit 9 in register 0x118 turns it off, providing a hint who the parent is.
+ */
+ GATE(ZX297520V3_CLK_NO_EXPORT, "lte-tpu-wclk", "matrix-dpll-d4", 0x60, 0, 0),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "lte-tpu-pclk", clk_main[0], 0x60, 2, 0),
+
+ GATE(ZX297520V3_EDCP_PCLK, "edcp-pclk", clk_main[0], 0x64, 1, 0),
+ GATE(ZX297520V3_EDCP_WCLK, "edcp-wclk", "edcp-mux", 0x64, 2, 0),
+
+ /* This seems to be another SPI-Like device. ZTE's firmware operates it and from testing
+ * it looks like the matrix-osc26m gate in top+0x140 stops this device. This would be
+ * consistent with the other SPI controllers too.
+ */
+ GATE(ZX297520V3_SSC_WCLK, "ssc-wclk", "matrix-osc26m", 0x84, 1, 0),
+ GATE(ZX297520V3_SSC_PCLK, "ssc-pclk", clk_main[0], 0x84, 2, 0),
+
+ /* PDCFG. Like PMM, either clock bit will allow the device to function. Probably there is
+ * no wclk line at all and the two bits are just an artifact of generally having two bits
+ * per device.
+ */
+ GATE(ZX297520V3_PDCFG_WCLK, "pdcfg-wclk", matrix_unk, 0x88, 0,
+ CLK_IS_CRITICAL),
+ GATE(ZX297520V3_PDCFG_PCLK, "pdcfg-pclk", clk_main[0], 0x88, 1,
+ CLK_IS_CRITICAL),
+ GATE(ZX297520V3_MBOX_PCLK, "mbox-pclk", clk_main[0], 0x88, 2, 0),
+ GATE(ZX297520V3_SRAM0_PCLK, "sram0-pclk", clk_main[0], 0x88, 4, 0),
+ GATE(ZX297520V3_GSM_CFG_PCLK, "gsm-cfg-pclk", clk_main[0], 0x88, 8, 0),
+
+ /* ZTE's driver has a statement to the effect of *(top->base+0x11c) = 5, with a comment
+ * suggesting that this sets a 50 MHz clock. The clock code itself lists GMAC clocks in
+ * matrix+0x110 and lists the parents of these clocks as 50 MHz gpll output, but
+ * the downstream ZTE GMAC driver never enables the clocks. It turns out ZTE's code is
+ * highly misleading.
+ *
+ * The GMAC's work clock is definitely not any gpll output because it keeps working fine
+ * with gpll disabled. Gating off matrix-osc26m breaks GMAC, so it must be its parent.
+ *
+ * The GMAC Gates are left enabled by the boot loader and are required for the GMAC to work.
+ *
+ * As for the 50 MHz comment: See rmiiphy-wclk.
+ */
+ GATE(ZX297520V3_GMAC_WCLK, "gmac-wclk", "matrix-osc26m", 0x110, 0, 0),
+ GATE(ZX297520V3_GMAC_PCLK, "gmac-pclk", clk_main[0], 0x110, 1, 0),
+ GATE(ZX297520V3_GMAC_AHB, "gmac-ahb", "ahb-wclk", 0x110, 2, 0),
+
+ /* Is there an AXI bus gate? The symptom of cutting off the AXI mux selection in top+0x140
+ * is that matrixcrm becomes unreadable from m0 and A53 hangs. Inside matrix itself only 3
+ * bits fit that bill: 0x8c bits 5, 6, 7. It seems a bit self-defeating to have a clock
+ * gate that shuts off access to itself though. I expect a clock gate for the bus
+ * somewhere, and the mux exists, so exposing one AXI clock in the bindings is the correct
+ * thing to do. It also serves to tell the kernel to keep the mux's parent enabled.
+ *
+ * Register 0x8c has 12 settable bits (0xfff). Ultimately it doesn't matter much which
+ * do-not-remove bit we don't remove. Other bits in this register behave like gates (e.g
+ * bits 11:8 cut off USB temporarily), so I think we are looking in the right place.
+ *
+ * If an explanation for the remaining bits surfaces and they are further gates and/or
+ * resets, add them to the bindings.
+ */
+ GATE(ZX297520V3_AXI_WCLK, "axi-wclk", "axi-mux", 0x8c, 5,
+ CLK_IS_CRITICAL),
+
+ GATE(ZX297520V3_DMA_PCLK, "dma-pclk", clk_main[0], 0x94, 3, 0),
+
+ /* There are a lot more VOU related controls in these registers, but turning off the main
+ * clock seems to shut off the entire VOU MMIO range.
+ */
+ GATE(ZX297520V3_VOU_WCLK, "vou-wclk", matrix_unk, 0x168, 0, 0),
+ GATE(ZX297520V3_VOU_PCLK, "vou-pclk", clk_main[0], 0x168, 1, 0),
+
+ /* LSP uplink clocks. The pclk is fairly obvious (disabling it shuts off the entire LSP
+ * register area). The wclk speeds were deduced by setting timers and qspi muxes to a
+ * specific speed and seeing which bit in matrix+0x7c needs to be enabled for the device
+ * to work.
+ *
+ * Due to the timers I am certain about the 26 MHz and 32 kHz clocks. I cannot directly
+ * observe the qspi mux frequency, so the clock rates depend on ZTE's qspi mux selection
+ * being correct.
+ *
+ * Two additional bits are specific to sound components - the mux for the LSP's TDM IP is
+ * in matrixclk and gets passed down. I2S has a mux in LSP, which can select the dpll-d4
+ * clock.
+ */
+ GATE(ZX297520V3_LSP_MPLL_D5_WCLK, "lsp-mpll-d5", "matrix-mpll-d5", 0x7c, 0, 0),
+ GATE(ZX297520V3_LSP_MPLL_D4_WCLK, "lsp-mpll-d4", "matrix-mpll-d4", 0x7c, 1, 0),
+ GATE(ZX297520V3_LSP_MPLL_D6_WCLK, "lsp-mpll-d6", "matrix-mpll-d6", 0x7c, 2, 0),
+ GATE(ZX297520V3_LSP_MPLL_D8_WCLK, "lsp-mpll-d8", "matrix-mpll-d8", 0x7c, 3, 0),
+ GATE(ZX297520V3_LSP_MPLL_D12_WCLK, "lsp-mpll-d12", "matrix-mpll-d12", 0x7c, 4, 0),
+ GATE(ZX297520V3_LSP_OSC26M_WCLK, "lsp-osc26m", "matrix-osc26m", 0x7c, 5, 0),
+ GATE(ZX297520V3_LSP_OSC32K_WCLK, "lsp-osc32k", "clock-32k", 0x7c, 6, 0),
+ GATE(ZX297520V3_LSP_PCLK, "lsp-pclk", clk_main[0], 0x7c, 7, 0),
+ GATE(ZX297520V3_LSP_TDM_WCLK, "lsp-tdm-wclk", "tdm-mux", 0x7c, 8, 0),
+ GATE(ZX297520V3_LSP_DPLL_D4_WCLK, "lsp-dpll-d4", "matrix-dpll-d4", 0x7c, 9, 0),
+};
+
+static const char * const zx297520v3_matrix_inputs[] = {
+ "osc26m", "osc32k",
+ "mpll", "mpll-d5", "dpll", "gpll", "gpll-d2", "wclk-osc26m",
+};
+
+static const struct zx_fixed_divider_desc zx297520v3_matrix_fixed_divs[] = {
+ {"matrix-osc26m-d4", "matrix-osc26m", 4},
+ {"matrix-mpll-d2-pregate", "matrix-mpll", 2},
+ {"matrix-mpll-d4-pregate", "matrix-mpll", 4},
+ /* div 5: provided as a separate line from topcrm */
+ {"matrix-mpll-d6-pregate", "matrix-mpll", 6},
+ {"matrix-mpll-d8-pregate", "matrix-mpll", 8},
+ {"matrix-mpll-d12-pregate", "matrix-mpll", 12},
+ {"matrix-mpll-d16-pregate", "matrix-mpll", 16},
+
+ {"matrix-gpll-d2-d2-pregate", "matrix-gpll-d2", 2},
+ {"matrix-gpll-d2-d4-pregate", "matrix-gpll-d2", 4},
+
+ {"matrix-dpll-d6-pregate", "matrix-dpll", 6},
+ {"matrix-dpll-d4-pregate", "matrix-dpll", 4},
+};
+
+static int zx297520v3_matrix_init(struct regmap *map)
+{
+ u32 axi_val;
+ int res;
+
+ /* Read the global AXI speed selection, insert it into our ballot and enable voting */
+ res = regmap_read(map, 0x0, &axi_val);
+ if (res)
+ return res;
+
+ axi_val &= 0x7;
+ res = regmap_write(map, ZX297520V3_AXI_AP_VOTE, axi_val);
+ if (res)
+ return res;
+
+ return regmap_write(map, 0x12c, 1);
+}
+
+static const struct zx_clk_data zx297520v3_matrixclk_data = {
+ .init = zx297520v3_matrix_init,
+ .inputs = zx297520v3_matrix_inputs,
+ .num_inputs = ARRAY_SIZE(zx297520v3_matrix_inputs),
+ .fixed_divs = zx297520v3_matrix_fixed_divs,
+ .num_fixed_divs = ARRAY_SIZE(zx297520v3_matrix_fixed_divs),
+ .muxes = zx297520v3_matrix_muxes,
+ .num_muxes = ARRAY_SIZE(zx297520v3_matrix_muxes),
+ .gates = zx297520v3_matrix_gates,
+ .num_gates = ARRAY_SIZE(zx297520v3_matrix_gates),
+};
+
static int clk_zx297520v3_probe(struct platform_device *pdev)
{
const struct platform_device_id *id = platform_get_device_id(pdev);
@@ -558,6 +892,10 @@ static const struct platform_device_id clk_zx297520v3_ids[] = {
.name = "zx297520v3-topclk",
.driver_data = (kernel_ulong_t)&zx297520v3_topclk_data,
},
+ {
+ .name = "zx297520v3-matrixclk",
+ .driver_data = (kernel_ulong_t)&zx297520v3_matrixclk_data,
+ },
{ }
};
MODULE_DEVICE_TABLE(platform, clk_zx297520v3_ids);
--
2.54.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* [PATCH v8 09/12] clk: zte: Introduce a driver for zx297520v3 matrix clocks
@ 2026-07-27 18:24 ` Stefan Dösinger
0 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
This clock controller controls high speed devices: CPU, DMA, RAM, SDIO,
Ethernet. The only non-clock, non-reset registers I have spotted here
are hardware spinlocks.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
Version 8:
Add another distribution gate register: 0x118 gates off matrix-generated
divisions of mpll, dpll and gpll. It nicely fits the top->matrix forward
picture.
AXI 6.5 MHz is not an mpll child. I must have done something wrong when
originally testing this
Add explanations about additional ARM architected timer controls.
Add gates for unknown LTE hardware at offset 0x60. They are not exported
in the bindings because the actual name of the consumer isn't clear -
but if dpll is unprepared and feeds a bypass 26 MHz signal, this device
starts spamming IRQs (although the 0x118 and top+0x140 gates also
prevent this usually)
Double-check pclk and wclk of all devices. The SDIO and EDCP clocks were
flipped in previous submissions.
Version 7:
Reduced the input clocks a lot. Most MPLL subdivisions are children of
the same gate in top, so put the fixed factor clocks here.
Added a few more clocks: DDR_CTRL, AXI bus, ARM architected timer.
They are needed to tell CCF to keep the top gates enabled and marked
critical because they aren't consumed by a DT node.
The AXI bus is determined by a voting system. I've set up the driver to
write to the ballot register ZTE's kernel uses and enable voting. This
should make life easier with the LTE rproc blob later.
Version 5:
Make it an MFD child device, remove aux device
Fix zx297529 -> zx297520 typos
Fix AHB_wclk -> ahb_wclk. That was a leftover of my old composite structs
Add SRAM0 and GSM_CFG clocks
---
drivers/clk/zte/clk-zx297520v3.c | 338 +++++++++++++++++++++++++++++++++++++++
1 file changed, 338 insertions(+)
diff --git a/drivers/clk/zte/clk-zx297520v3.c b/drivers/clk/zte/clk-zx297520v3.c
index 4444c2eb1382..9299449097b4 100644
--- a/drivers/clk/zte/clk-zx297520v3.c
+++ b/drivers/clk/zte/clk-zx297520v3.c
@@ -542,6 +542,340 @@ static const struct zx_clk_data zx297520v3_topclk_data = {
.num_gates = ARRAY_SIZE(zx297520v3_top_gates),
};
+/* For devices which have a working driver the work clock can be figured out by gating off registers
+ * in top+0x140. This is used for devices where I can't interpret the register contents yet.
+ */
+static const char matrix_unk[] = "matrix-osc26m";
+
+static const char * const cpu_sel[] = {
+ "matrix-osc26m",
+ "matrix-mpll", /* 624 MHz */
+ "matrix-mpll-d2", /* 312 MHz */
+ "matrix-mpll-d4", /* 156 MHz */
+};
+
+/* Low confidence on the actual value, but at least I know it is bit 13 in top+0x140 */
+static const char * const zsp_sel[] = {
+ "matrix-osc26m",
+ "matrix-dpll", /* 491.52 MHz */
+};
+
+/* We can't realistically change DDR speed while running an OS out of DDR (it reads garbage for a
+ * short while on transition), but we need to know if we have to keep gpll alive. 32 MB devices use
+ * 200 MHz, while 64/128 MB ones use 156 MHz.
+ */
+static const char * const ddr_ctrl_sel[] = {
+ "matrix-mpll-d4", /* 156 MHz */
+ "matrix-gpll", /* 200 MHz */
+ "matrix-mpll-d6", /* 104 MHz */
+ "matrix-mpll-d8", /* 78 MHz */
+};
+
+static const char * const sd0_sel[] = {
+ "matrix-osc26m",
+ "matrix-mpll-d4", /* 156 MHz */
+ "matrix-gpll-d2", /* 100 MHz */
+ "matrix-mpll-d8", /* 78 MHz */
+ "matrix-gpll-d2-d2", /* 50 MHz */
+ "matrix-gpll-d2-d4", /* 25 MHz */
+};
+
+static const char * const sd1_sel[] = {
+ "matrix-osc26m",
+ "matrix-gpll-d2", /* 100 MHz */
+ "matrix-mpll-d8", /* 78 MHz */
+ "matrix-gpll-d2-d2", /* 50 MHz */
+ "matrix-mpll-d16", /* 39 MHz */
+ "matrix-gpll-d2-d4", /* 25 MHz */
+};
+
+/* ZTE's kernel puts the high frequency first, but this is against the usual convention on this
+ * SoC. I don't have any device that has a raw NAND controller though.
+ */
+static const char * const nand_sel[] = {
+ "matrix-osc26m",
+ "matrix-mpll-d4", /* 156 MHz */
+};
+
+static const char * const edcp_sel[] = {
+ "matrix-osc26m",
+ "matrix-mpll-d4", /* 156 MHz */
+ "matrix-mpll-d5", /* 124.8 MHz */
+ "matrix-mpll-d6", /* 104 MHz */
+};
+
+static const char * const tdm_sel[] = {
+ "matrix-osc26m",
+ "matrix-dpll-d4", /* 122.88 MHz */
+ "matrix-mpll-d6", /* 104 MHz */
+};
+
+/* ZTE's firmware uses 0x128 as the PHY's vote and names 0x120 as "PS" and 0x124 as "AP" vote.
+ * However, their cpufreq driver ultimately uses the "PS" define and thus writes to 0x120, which can
+ * be observed in the running system.
+ */
+#define ZX297520V3_AXI_AP_VOTE 0x120
+static const char * const axi_sel[] = {
+ "matrix-osc26m",
+ "matrix-mpll-d4", /* 156 MHz */
+ "matrix-mpll-d5", /* 124.8 MHz */
+ "matrix-mpll-d6", /* 104 MHz */
+ "matrix-mpll-d8", /* 78 MHz */
+ "matrix-mpll-d12", /* 52 MHz */
+ "matrix-mpll-d16", /* 39 MHz */
+ "matrix-osc26m-d4", /* 6.5 MHz */
+};
+
+static const struct zx_mux_desc zx297520v3_matrix_muxes[] = {
+ MUX(ZX297520V3_CLK_NO_EXPORT, "cpu-mux", cpu_sel, 0x20, 0, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "zsp-mux", zsp_sel, 0x30, 0, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "ddr-ctrl-mux", ddr_ctrl_sel, 0x50, 0, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "sd0-mux", sd0_sel, 0x50, 4, 3),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "sd1-mux", sd1_sel, 0x50, 8, 3),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "nand-mux", nand_sel, 0x50, 12, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "edcp-mux", edcp_sel, 0x50, 16, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "tdm-mux", tdm_sel, 0x50, 24, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "axi-mux", axi_sel, ZX297520V3_AXI_AP_VOTE, 0, 3),
+};
+
+static const struct zx_gate_desc zx297520v3_matrix_gates[] = {
+ /* Matrix subdivision gates: Almost every matrix-generated subdivision of the input PLLs
+ * has a gate in 0x118. The original inputs from topcrm (mpll 624, mpll 124.8, gpll 200,
+ * gpll 100, dpll 491.52) do not.
+ *
+ * I do not know of any user of dpll-d6 and do not know the divisor. An 81.92 MHz clock is
+ * listed as a root clock without a consumer in ZTE's code. Most likely it is a possible
+ * parent of the ZSP mux.
+ */
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-mpll-d16", "matrix-mpll-d16-pregate",
+ 0x118, 0, ZX297520V3_AXI_CANDIDATE),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-mpll-d12", "matrix-mpll-d12-pregate",
+ 0x118, 1, ZX297520V3_AXI_CANDIDATE),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-mpll-d8", "matrix-mpll-d8-pregate",
+ 0x118, 2, ZX297520V3_AXI_CANDIDATE),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-mpll-d6", "matrix-mpll-d6-pregate",
+ 0x118, 3, ZX297520V3_AXI_CANDIDATE),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-mpll-d4", "matrix-mpll-d4-pregate",
+ 0x118, 4, ZX297520V3_AXI_CANDIDATE),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-mpll-d2", "matrix-mpll-d2-pregate",
+ 0x118, 5, 0),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-dpll-d6", "matrix-dpll-d6-pregate",
+ 0x118, 8, 0),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-dpll-d4", "matrix-dpll-d4-pregate",
+ 0x118, 9, 0),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-gpll-d2-d4", "matrix-gpll-d2-d4-pregate",
+ 0x118, 12, 0),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "matrix-gpll-d2-d2", "matrix-gpll-d2-d2-pregate",
+ 0x118, 13, 0),
+
+ /* This bit cuts off the clock signal to the ARM architected timer, which the kernel uses
+ * as its main timer. It isn't critical per se - there are plenty of proprietary timers
+ * available that could be used - but the arm arch timer binding does not accept a clock, so
+ * this CCF driver won't know if the timer is in use.
+ *
+ * This clock is fed by the mux in topclk+0x140 - this critical clock here protects its
+ * parent.
+ *
+ * Registers 0x148, 0x14c, 0x150, 0x154 allow setting and clearing the timer counter. To
+ * set the timer, load a value into 0x148 (high 32 bits) and 0x14c (low 32 bits), then
+ * toggle 0x150 to 1. 0x150 will automatically reset to 0. 0x154 sets the timer to some
+ * "clear" value, but this value isn't 0.
+ */
+ GATE(ZX297520V3_SYS_TIMER_WCLK, "sys-timer-wclk", "matrix-osc26m", 0x144, 1,
+ CLK_IS_CRITICAL),
+
+ /* Both 0x24 and 0x28 bits 1 and 2 stop the CPU. There is also a bit in topclk+0x138, which
+ * ZTE's uboot calls "A53 reset", which also stops the CPU. I can't really tell the
+ * difference between matrix+0x28 and top+0x138. The clock (matrix+0x24) can be disabled
+ * and enabled from the Cortex M0 and it will nicely stop and restart the A53, retaining
+ * all state.
+ */
+ GATE(ZX297520V3_CPU_WCLK, "cpu-wclk", "cpu-mux", 0x24, 1,
+ CLK_IS_CRITICAL),
+ GATE(ZX297520V3_CPU_PCLK, "cpu-pclk", clk_main[0], 0x24, 2,
+ CLK_IS_CRITICAL),
+
+ /* There are a lot more controls in matrix+0x100. 13-16 appear to be 4 different AXI
+ * channels for different priorities. Bit 19 appears to be the DDR PHY wclk.
+ *
+ * The important task is to keep gpll enabled if the bootloader selected a gpll-based rate
+ * for RAM.
+ */
+ GATE(ZX297520V3_DDR_CTRL_PCLK, "ddr-ctrl-pclk", clk_main[0], 0x100, 17,
+ CLK_IS_CRITICAL),
+ GATE(ZX297520V3_DDR_CTRL_WCLK, "ddr-ctrl-wclk", "ddr-ctrl-mux", 0x100, 18,
+ CLK_IS_CRITICAL),
+
+ GATE(ZX297520V3_ZSP_WCLK, "zsp-wclk", "zsp-mux", 0x3c, 0, 0),
+
+ GATE(ZX297520V3_SD0_PCLK, "sd0-pclk", clk_main[0], 0x54, 12, 0),
+ GATE(ZX297520V3_SD0_WCLK, "sd0-wclk", "sd0-mux", 0x54, 13, 0),
+ GATE(ZX297520V3_SD0_CDET, "sd0-cdet", "clock-32k", 0x54, 14, 0),
+ GATE(ZX297520V3_SD1_PCLK, "sd1-pclk", clk_main[0], 0x54, 4, 0),
+ GATE(ZX297520V3_SD1_WCLK, "sd1-wclk", "sd1-mux", 0x54, 5, 0),
+ /* I don't know how the cdet clock works. Card detection in the way the dwc,mmc driver uses
+ * it appears broken no matter this clock's setting.
+ */
+ GATE(ZX297520V3_SD1_CDET, "sd1-cdet", "clock-32k", 0x54, 6, 0),
+
+ /* This is some "denali" NAND, not the qspi connected one */
+ GATE(ZX297520V3_NAND_WCLK, "nand-wclk", "nand-mux", 0x54, 20, 0),
+ GATE(ZX297520V3_NAND_PCLK, "nand-pclk", clk_main[0], 0x54, 21, 0),
+
+ /* There is a set of gates for an unknown device at matrix+0x60. It is some LTE related
+ * device. If it is fed an incorrect clock, e.g. because dpll is not locked and outputs
+ * 26 MHz, it will spam IRQs at SPI 68. This is likely a distress notification that some
+ * internal PLL did not lock or similar
+ *
+ * ZTE's kernel has two defines for SPI 64: "LTE_TPU_INT_PS_INT" and "VOU_OSD_INT". The
+ * latter is unlikely because VOU related clocks sit in matrix+0x168.
+ *
+ * Bit 9 in register 0x118 turns it off, providing a hint who the parent is.
+ */
+ GATE(ZX297520V3_CLK_NO_EXPORT, "lte-tpu-wclk", "matrix-dpll-d4", 0x60, 0, 0),
+ GATE(ZX297520V3_CLK_NO_EXPORT, "lte-tpu-pclk", clk_main[0], 0x60, 2, 0),
+
+ GATE(ZX297520V3_EDCP_PCLK, "edcp-pclk", clk_main[0], 0x64, 1, 0),
+ GATE(ZX297520V3_EDCP_WCLK, "edcp-wclk", "edcp-mux", 0x64, 2, 0),
+
+ /* This seems to be another SPI-Like device. ZTE's firmware operates it and from testing
+ * it looks like the matrix-osc26m gate in top+0x140 stops this device. This would be
+ * consistent with the other SPI controllers too.
+ */
+ GATE(ZX297520V3_SSC_WCLK, "ssc-wclk", "matrix-osc26m", 0x84, 1, 0),
+ GATE(ZX297520V3_SSC_PCLK, "ssc-pclk", clk_main[0], 0x84, 2, 0),
+
+ /* PDCFG. Like PMM, either clock bit will allow the device to function. Probably there is
+ * no wclk line at all and the two bits are just an artifact of generally having two bits
+ * per device.
+ */
+ GATE(ZX297520V3_PDCFG_WCLK, "pdcfg-wclk", matrix_unk, 0x88, 0,
+ CLK_IS_CRITICAL),
+ GATE(ZX297520V3_PDCFG_PCLK, "pdcfg-pclk", clk_main[0], 0x88, 1,
+ CLK_IS_CRITICAL),
+ GATE(ZX297520V3_MBOX_PCLK, "mbox-pclk", clk_main[0], 0x88, 2, 0),
+ GATE(ZX297520V3_SRAM0_PCLK, "sram0-pclk", clk_main[0], 0x88, 4, 0),
+ GATE(ZX297520V3_GSM_CFG_PCLK, "gsm-cfg-pclk", clk_main[0], 0x88, 8, 0),
+
+ /* ZTE's driver has a statement to the effect of *(top->base+0x11c) = 5, with a comment
+ * suggesting that this sets a 50 MHz clock. The clock code itself lists GMAC clocks in
+ * matrix+0x110 and lists the parents of these clocks as 50 MHz gpll output, but
+ * the downstream ZTE GMAC driver never enables the clocks. It turns out ZTE's code is
+ * highly misleading.
+ *
+ * The GMAC's work clock is definitely not any gpll output because it keeps working fine
+ * with gpll disabled. Gating off matrix-osc26m breaks GMAC, so it must be its parent.
+ *
+ * The GMAC Gates are left enabled by the boot loader and are required for the GMAC to work.
+ *
+ * As for the 50 MHz comment: See rmiiphy-wclk.
+ */
+ GATE(ZX297520V3_GMAC_WCLK, "gmac-wclk", "matrix-osc26m", 0x110, 0, 0),
+ GATE(ZX297520V3_GMAC_PCLK, "gmac-pclk", clk_main[0], 0x110, 1, 0),
+ GATE(ZX297520V3_GMAC_AHB, "gmac-ahb", "ahb-wclk", 0x110, 2, 0),
+
+ /* Is there an AXI bus gate? The symptom of cutting off the AXI mux selection in top+0x140
+ * is that matrixcrm becomes unreadable from m0 and A53 hangs. Inside matrix itself only 3
+ * bits fit that bill: 0x8c bits 5, 6, 7. It seems a bit self-defeating to have a clock
+ * gate that shuts off access to itself though. I expect a clock gate for the bus
+ * somewhere, and the mux exists, so exposing one AXI clock in the bindings is the correct
+ * thing to do. It also serves to tell the kernel to keep the mux's parent enabled.
+ *
+ * Register 0x8c has 12 settable bits (0xfff). Ultimately it doesn't matter much which
+ * do-not-remove bit we don't remove. Other bits in this register behave like gates (e.g
+ * bits 11:8 cut off USB temporarily), so I think we are looking in the right place.
+ *
+ * If an explanation for the remaining bits surfaces and they are further gates and/or
+ * resets, add them to the bindings.
+ */
+ GATE(ZX297520V3_AXI_WCLK, "axi-wclk", "axi-mux", 0x8c, 5,
+ CLK_IS_CRITICAL),
+
+ GATE(ZX297520V3_DMA_PCLK, "dma-pclk", clk_main[0], 0x94, 3, 0),
+
+ /* There are a lot more VOU related controls in these registers, but turning off the main
+ * clock seems to shut off the entire VOU MMIO range.
+ */
+ GATE(ZX297520V3_VOU_WCLK, "vou-wclk", matrix_unk, 0x168, 0, 0),
+ GATE(ZX297520V3_VOU_PCLK, "vou-pclk", clk_main[0], 0x168, 1, 0),
+
+ /* LSP uplink clocks. The pclk is fairly obvious (disabling it shuts off the entire LSP
+ * register area). The wclk speeds were deduced by setting timers and qspi muxes to a
+ * specific speed and seeing which bit in matrix+0x7c needs to be enabled for the device
+ * to work.
+ *
+ * Due to the timers I am certain about the 26 MHz and 32 kHz clocks. I cannot directly
+ * observe the qspi mux frequency, so the clock rates depend on ZTE's qspi mux selection
+ * being correct.
+ *
+ * Two additional bits are specific to sound components - the mux for the LSP's TDM IP is
+ * in matrixclk and gets passed down. I2S has a mux in LSP, which can select the dpll-d4
+ * clock.
+ */
+ GATE(ZX297520V3_LSP_MPLL_D5_WCLK, "lsp-mpll-d5", "matrix-mpll-d5", 0x7c, 0, 0),
+ GATE(ZX297520V3_LSP_MPLL_D4_WCLK, "lsp-mpll-d4", "matrix-mpll-d4", 0x7c, 1, 0),
+ GATE(ZX297520V3_LSP_MPLL_D6_WCLK, "lsp-mpll-d6", "matrix-mpll-d6", 0x7c, 2, 0),
+ GATE(ZX297520V3_LSP_MPLL_D8_WCLK, "lsp-mpll-d8", "matrix-mpll-d8", 0x7c, 3, 0),
+ GATE(ZX297520V3_LSP_MPLL_D12_WCLK, "lsp-mpll-d12", "matrix-mpll-d12", 0x7c, 4, 0),
+ GATE(ZX297520V3_LSP_OSC26M_WCLK, "lsp-osc26m", "matrix-osc26m", 0x7c, 5, 0),
+ GATE(ZX297520V3_LSP_OSC32K_WCLK, "lsp-osc32k", "clock-32k", 0x7c, 6, 0),
+ GATE(ZX297520V3_LSP_PCLK, "lsp-pclk", clk_main[0], 0x7c, 7, 0),
+ GATE(ZX297520V3_LSP_TDM_WCLK, "lsp-tdm-wclk", "tdm-mux", 0x7c, 8, 0),
+ GATE(ZX297520V3_LSP_DPLL_D4_WCLK, "lsp-dpll-d4", "matrix-dpll-d4", 0x7c, 9, 0),
+};
+
+static const char * const zx297520v3_matrix_inputs[] = {
+ "osc26m", "osc32k",
+ "mpll", "mpll-d5", "dpll", "gpll", "gpll-d2", "wclk-osc26m",
+};
+
+static const struct zx_fixed_divider_desc zx297520v3_matrix_fixed_divs[] = {
+ {"matrix-osc26m-d4", "matrix-osc26m", 4},
+ {"matrix-mpll-d2-pregate", "matrix-mpll", 2},
+ {"matrix-mpll-d4-pregate", "matrix-mpll", 4},
+ /* div 5: provided as a separate line from topcrm */
+ {"matrix-mpll-d6-pregate", "matrix-mpll", 6},
+ {"matrix-mpll-d8-pregate", "matrix-mpll", 8},
+ {"matrix-mpll-d12-pregate", "matrix-mpll", 12},
+ {"matrix-mpll-d16-pregate", "matrix-mpll", 16},
+
+ {"matrix-gpll-d2-d2-pregate", "matrix-gpll-d2", 2},
+ {"matrix-gpll-d2-d4-pregate", "matrix-gpll-d2", 4},
+
+ {"matrix-dpll-d6-pregate", "matrix-dpll", 6},
+ {"matrix-dpll-d4-pregate", "matrix-dpll", 4},
+};
+
+static int zx297520v3_matrix_init(struct regmap *map)
+{
+ u32 axi_val;
+ int res;
+
+ /* Read the global AXI speed selection, insert it into our ballot and enable voting */
+ res = regmap_read(map, 0x0, &axi_val);
+ if (res)
+ return res;
+
+ axi_val &= 0x7;
+ res = regmap_write(map, ZX297520V3_AXI_AP_VOTE, axi_val);
+ if (res)
+ return res;
+
+ return regmap_write(map, 0x12c, 1);
+}
+
+static const struct zx_clk_data zx297520v3_matrixclk_data = {
+ .init = zx297520v3_matrix_init,
+ .inputs = zx297520v3_matrix_inputs,
+ .num_inputs = ARRAY_SIZE(zx297520v3_matrix_inputs),
+ .fixed_divs = zx297520v3_matrix_fixed_divs,
+ .num_fixed_divs = ARRAY_SIZE(zx297520v3_matrix_fixed_divs),
+ .muxes = zx297520v3_matrix_muxes,
+ .num_muxes = ARRAY_SIZE(zx297520v3_matrix_muxes),
+ .gates = zx297520v3_matrix_gates,
+ .num_gates = ARRAY_SIZE(zx297520v3_matrix_gates),
+};
+
static int clk_zx297520v3_probe(struct platform_device *pdev)
{
const struct platform_device_id *id = platform_get_device_id(pdev);
@@ -558,6 +892,10 @@ static const struct platform_device_id clk_zx297520v3_ids[] = {
.name = "zx297520v3-topclk",
.driver_data = (kernel_ulong_t)&zx297520v3_topclk_data,
},
+ {
+ .name = "zx297520v3-matrixclk",
+ .driver_data = (kernel_ulong_t)&zx297520v3_matrixclk_data,
+ },
{ }
};
MODULE_DEVICE_TABLE(platform, clk_zx297520v3_ids);
--
2.54.0
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 50+ messages in thread* Re: [PATCH v8 09/12] clk: zte: Introduce a driver for zx297520v3 matrix clocks
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 19:41 ` sashiko-bot
-1 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 19:41 UTC (permalink / raw)
To: Stefan Dösinger
Cc: neil.armstrong, devicetree, robh, mfd, vkoul, olteanv, conor+dt,
linux-phy, lee
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] The use of `clk_main[0]` to initialize fields in static global arrays is invalid C and causes a compilation error.
--
--- Patch [9]: [PATCH v8 09/12] clk: zte: Introduce a driver for zx297520v3 matrix clocks ---
commit 895d095e9e21d84eaefefb2e2881a4f8193ef450
Author: Stefan Dösinger <stefandoesinger@gmail.com>
clk: zte: Introduce a driver for zx297520v3 matrix clocks
This commit introduces a new clock driver for the zx297520v3 matrix clock
controller, which manages high-speed devices such as CPU, DMA, RAM, SDIO,
and Ethernet. It registers multiplexers, fixed dividers, and gates within
the controller.
> diff --git a/drivers/clk/zte/clk-zx297520v3.c b/drivers/clk/zte/clk-zx297520v3.c
> index 4444c2eb13825..9299449097b4a 100644
> --- a/drivers/clk/zte/clk-zx297520v3.c
> +++ b/drivers/clk/zte/clk-zx297520v3.c
[ ... ]
> @@ -542,6 +542,340 @@ static const struct zx_clk_data zx297520v3_topclk_data = {
[ ... ]
> +static const struct zx_gate_desc zx297520v3_matrix_gates[] = {
[ ... ]
> + /* Both 0x24 and 0x28 bits 1 and 2 stop the CPU. There is also a bit in topclk+0x138, which
> + * ZTE's uboot calls "A53 reset", which also stops the CPU. I can't really tell the
> + * difference between matrix+0x28 and top+0x138. The clock (matrix+0x24) can be disabled
> + * and enabled from the Cortex M0 and it will nicely stop and restart the A53, retaining
> + * all state.
> + */
> + GATE(ZX297520V3_CPU_WCLK, "cpu-wclk", "cpu-mux", 0x24, 1,
> + CLK_IS_CRITICAL),
> + GATE(ZX297520V3_CPU_PCLK, "cpu-pclk", clk_main[0], 0x24, 2,
> + CLK_IS_CRITICAL),
[Severity: Low]
Does this cause a compilation error? In C, static array initializers must
use constant expressions. Accessing an array element like clk_main[0] requires
reading memory, which is not considered a constant expression by the compiler,
and will likely result in an initializer element is not constant build error.
This pattern also appears in several other GATE definitions further down in
the zx297520v3_matrix_gates array.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=9
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 50+ messages in thread* Re: [PATCH v8 09/12] clk: zte: Introduce a driver for zx297520v3 matrix clocks
@ 2026-07-27 19:41 ` sashiko-bot
0 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 19:41 UTC (permalink / raw)
To: Stefan Dösinger
Cc: neil.armstrong, devicetree, robh, mfd, vkoul, olteanv, conor+dt,
linux-phy, lee
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] The use of `clk_main[0]` to initialize fields in static global arrays is invalid C and causes a compilation error.
--
--- Patch [9]: [PATCH v8 09/12] clk: zte: Introduce a driver for zx297520v3 matrix clocks ---
commit 895d095e9e21d84eaefefb2e2881a4f8193ef450
Author: Stefan Dösinger <stefandoesinger@gmail.com>
clk: zte: Introduce a driver for zx297520v3 matrix clocks
This commit introduces a new clock driver for the zx297520v3 matrix clock
controller, which manages high-speed devices such as CPU, DMA, RAM, SDIO,
and Ethernet. It registers multiplexers, fixed dividers, and gates within
the controller.
> diff --git a/drivers/clk/zte/clk-zx297520v3.c b/drivers/clk/zte/clk-zx297520v3.c
> index 4444c2eb13825..9299449097b4a 100644
> --- a/drivers/clk/zte/clk-zx297520v3.c
> +++ b/drivers/clk/zte/clk-zx297520v3.c
[ ... ]
> @@ -542,6 +542,340 @@ static const struct zx_clk_data zx297520v3_topclk_data = {
[ ... ]
> +static const struct zx_gate_desc zx297520v3_matrix_gates[] = {
[ ... ]
> + /* Both 0x24 and 0x28 bits 1 and 2 stop the CPU. There is also a bit in topclk+0x138, which
> + * ZTE's uboot calls "A53 reset", which also stops the CPU. I can't really tell the
> + * difference between matrix+0x28 and top+0x138. The clock (matrix+0x24) can be disabled
> + * and enabled from the Cortex M0 and it will nicely stop and restart the A53, retaining
> + * all state.
> + */
> + GATE(ZX297520V3_CPU_WCLK, "cpu-wclk", "cpu-mux", 0x24, 1,
> + CLK_IS_CRITICAL),
> + GATE(ZX297520V3_CPU_PCLK, "cpu-pclk", clk_main[0], 0x24, 2,
> + CLK_IS_CRITICAL),
[Severity: Low]
Does this cause a compilation error? In C, static array initializers must
use constant expressions. Accessing an array element like clk_main[0] requires
reading memory, which is not considered a constant expression by the compiler,
and will likely result in an initializer element is not constant build error.
This pattern also appears in several other GATE definitions further down in
the zx297520v3_matrix_gates array.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=9
^ permalink raw reply [flat|nested] 50+ messages in thread
* [PATCH v8 10/12] clk: zte: Introduce a driver for zx297520v3 LSP clocks
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 18:24 ` Stefan Dösinger
-1 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
"LSP" is ZTE's term for this part of the SoC, I suspect it stands for
"low speed peripherals". The main UART is here, together with the flash
controller and more surplus proprietary timers.
It also has one more I2C controller that supposedly connects to a
battery charger, SPI for displays and I2S for analog telephones. The
boards I have don't have any of these components though.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
Version 8:
Double-checked pclk and wclk. They were the wrong way around in previous
submissions. Pclk is bit 0, wclk is bit 1.
Version 7:
Underscore in clock names to dashes
Version 6:
use MFD for LSP too
Version 5:
Removed unused tdm_lsp_sel. Fix zx297529 vs zx297520 typo.
Adjusted .compatible to zte,zx297520v3-lspcrm
---
drivers/clk/zte/clk-zx297520v3.c | 189 +++++++++++++++++++++++++++++++++++++++
1 file changed, 189 insertions(+)
diff --git a/drivers/clk/zte/clk-zx297520v3.c b/drivers/clk/zte/clk-zx297520v3.c
index 9299449097b4..2fe7ac389325 100644
--- a/drivers/clk/zte/clk-zx297520v3.c
+++ b/drivers/clk/zte/clk-zx297520v3.c
@@ -876,6 +876,191 @@ static const struct zx_clk_data zx297520v3_matrixclk_data = {
.num_gates = ARRAY_SIZE(zx297520v3_matrix_gates),
};
+/* LSP clock entries have a common pattern: Bit 0 for pclk, Bit 1 for wclk. Bit 4 (and sometimes
+ * more) for wclk mux.
+ *
+ * Bit 8 and 9 are resets handled by the reset-zte-zx297520v3 driver.
+ *
+ * Bits 15:12 can be a divisor, but not all clocks have it. Some clocks have a divisor in 19:16.
+ *
+ * The ID given in this table is the first register in the device's MMIO space. ZTE's drivers
+ * usually call this a version register, but it looks more like a device identifier.
+ *
+ * It looks like the registers map to devices like this:
+ *
+ * Timer reg function div dev offset(lsp + xxxx) ID
+ * 0x0: Read-only, probably device identifier 0x00752100
+ * 0x4: timer_l1 Y 0x1000 0x02020000
+ * 0x8: watchdog_l2 Y 0x2000 0x02020000
+ * 0xc: watchdog_l3 Y 0x3000 0x02020000
+ * 0x10: pwm N 0x4000 0x01020000
+ * 0x14: i2s0 Yh 0x5000 0x01030000
+ * 0x18: always 0 - - -
+ * 0x1c: i2s1 Yh 0x6000 0x01030000
+ * 0x20: always 0 - - -
+ * 0x24: qspi N 0x7000 0x01040000
+ * 0x28: uart1 N 0x8000 0x01060000
+ * 0x2c: i2c1 N 0x9000 0x01020000
+ * 0x30: spi0 Y 0xa000 0x01040000
+ * 0x34: timer_lb Y 0xb000 0x02020000
+ * 0x38: timer_lc Y 0xc000 0x02020000
+ * 0x3c: uart2 N 0xd000 0x01060000
+ * 0x40: watchdog_le Y 0xe000 0x02020000
+ * 0x44: timer_lf Y 0xf000 0x02020000
+ * 0x48: spi1 Y 0x10000 0x01040000
+ * 0x4c: timer_l11 Y 0x11000 0x02020000
+ * 0x50: tdm Yh 0x12000 0x01040000
+ *
+ * Registers 0x58, 0x5c, 0x60, 0x64, 0x68 seem to contain more controls for i2s and tdm.
+ *
+ * I am not sure about the device at offset 0x4000 (clk reg 0x10). The ID matches that of i2c, but
+ * it has a larger register set. I suspect it is a PWM device, but I have not seen any ZTE kernel
+ * operate it - even devices with displays only use a GPIO to control the backlight.
+ */
+
+static const char * const timer_lsp_sel[] = {
+ "lsp-osc32k",
+ "lsp-osc26m",
+};
+
+static const char * const uart_lsp_sel[] = {
+ "lsp-osc26m",
+ "lsp-mpll-d6",
+};
+
+static const char * const i2s_lsp_sel[] = {
+ "lsp-osc26m",
+ "lsp-dpll-d4",
+ "lsp-mpll-d6",
+ /* Unknown */
+};
+
+static const char * const spi_lsp_sel[] = {
+ "lsp-osc26m",
+ "lsp-mpll-d4",
+ "lsp-mpll-d6",
+ /* Unknown */
+};
+
+static const char * const qspi_lsp_sel[] = {
+ "lsp-osc26m",
+ "lsp-mpll-d4",
+ "lsp-mpll-d5",
+ "lsp-mpll-d6",
+ "lsp-mpll-d8",
+ "lsp-mpll-d12",
+ "lsp-osc26m",
+ "lsp-osc26m",
+};
+
+static const struct zx_mux_desc zx297520v3_lsp_muxes[] = {
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-l1-mux", timer_lsp_sel, 0x04, 4, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "wdt-l2-mux", timer_lsp_sel, 0x08, 4, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "wdt-l3-mux", timer_lsp_sel, 0x0c, 4, 1),
+ /* PWM: No mux bit can be set */
+ MUX(ZX297520V3_CLK_NO_EXPORT, "i2s0-mux", i2s_lsp_sel, 0x14, 4, 2),
+ /* 0x18: Always 0 */
+ MUX(ZX297520V3_CLK_NO_EXPORT, "i2s1-mux", i2s_lsp_sel, 0x1c, 4, 2),
+ /* 0x20: Always 0 */
+ MUX(ZX297520V3_CLK_NO_EXPORT, "qspi-mux", qspi_lsp_sel, 0x24, 4, 3),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "uart1-mux", uart_lsp_sel, 0x28, 4, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "i2c1-mux", uart_lsp_sel, 0x2c, 4, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "spi0-mux", spi_lsp_sel, 0x30, 4, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-lb-mux", timer_lsp_sel, 0x34, 4, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-lc-mux", timer_lsp_sel, 0x38, 4, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "uart2-mux", uart_lsp_sel, 0x3c, 4, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "wdt-le-mux", timer_lsp_sel, 0x40, 4, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-lf-mux", timer_lsp_sel, 0x44, 4, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "spi1-mux", spi_lsp_sel, 0x48, 4, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-l11-mux", timer_lsp_sel, 0x4c, 4, 1),
+ /* TDM: No mux in LSP. Instead, it is in matrix with a separate clk line to LSP */
+};
+
+static const struct zx_div_desc zx297520v3_lsp_dividers[] = {
+ DIV("timer-l1-div", "timer-l1-mux", 0x04, 12, 4),
+ DIV("wdt-l2-div", "wdt-l2-mux", 0x08, 12, 4),
+ DIV("wdt-l3-div", "wdt-l3-mux", 0x0c, 12, 4),
+ /* PWM: No div */
+ DIV("i2s0-div", "i2s0-mux", 0x14, 16, 4),
+ /* 0x18: Always 0 */
+ DIV("i2s1-div", "i2s1-mux", 0x1c, 16, 4),
+ /* 0x20: Always 0 */
+ /* qspi, uart1, i2c1: No div */
+ DIV("spi0-div", "spi0-mux", 0x30, 12, 4),
+ DIV("timer-lb-div", "timer-lb-mux", 0x34, 12, 4),
+ DIV("timer-lc-div", "timer-lc-mux", 0x38, 12, 4),
+ /* uart2: No div */
+ DIV("wdt-le-div", "wdt-le-mux", 0x40, 12, 4),
+ DIV("timer-lf-div", "timer-lf-mux", 0x44, 12, 4),
+ DIV("spi1-div", "spi1-mux", 0x48, 12, 4),
+ DIV("timer-l11-div", "timer-l11-mux", 0x4c, 12, 4),
+ DIV("tdm-div", "lsp-tdm-wclk", 0x50, 16, 4),
+};
+
+static const struct zx_gate_desc zx297520v3_lsp_gates[] = {
+ GATE(ZX297520V3_TIMER_L1_PCLK, "timer-l1-pclk", "lsp-pclk", 0x04, 0, 0),
+ GATE(ZX297520V3_TIMER_L1_WCLK, "timer-l1-wclk", "timer-l1-div", 0x04, 1, 0),
+ GATE(ZX297520V3_WDT_L2_PCLK, "wdt-l2-pclk", "lsp-pclk", 0x08, 0, 0),
+ GATE(ZX297520V3_WDT_L2_WCLK, "wdt-l2-wclk", "wdt-l2-div", 0x08, 1, 0),
+ GATE(ZX297520V3_WDT_L3_PCLK, "wdt-l3-pclk", "lsp-pclk", 0x0c, 0, 0),
+ GATE(ZX297520V3_WDT_L3_WCLK, "wdt-l3-wclk", "wdt-l3-div", 0x0c, 1, 0),
+ /* I don't know the LSP parent. It must be one of the LSP inputs though. */
+ GATE(ZX297520V3_PWM_PCLK, "pwm-pclk", "lsp-pclk", 0x10, 0, 0),
+ GATE(ZX297520V3_PWM_WCLK, "pwm-wclk", "lsp-osc26m", 0x10, 1, 0),
+ GATE(ZX297520V3_I2S0_PCLK, "i2s0-pclk", "lsp-pclk", 0x14, 0, 0),
+ GATE(ZX297520V3_I2S0_WCLK, "i2s0-wclk", "i2s0-div", 0x14, 1, 0),
+ /* 0x18: Always 0 */
+ GATE(ZX297520V3_I2S1_PCLK, "i2s1-pclk", "lsp-pclk", 0x1c, 0, 0),
+ GATE(ZX297520V3_I2S1_WCLK, "i2s1-wclk", "i2s1-div", 0x1c, 1, 0),
+ /* 0x20: Always 0 */
+ GATE(ZX297520V3_QSPI_PCLK, "qspi-pclk", "lsp-pclk", 0x24, 0, 0),
+ GATE(ZX297520V3_QSPI_WCLK, "qspi-wclk", "qspi-mux", 0x24, 1, 0),
+ GATE(ZX297520V3_UART1_PCLK, "uart1-pclk", "lsp-pclk", 0x28, 0, 0),
+ GATE(ZX297520V3_UART1_WCLK, "uart1-wclk", "uart1-mux", 0x28, 1, 0),
+ GATE(ZX297520V3_I2C1_PCLK, "i2c1-pclk", "lsp-pclk", 0x2c, 0, 0),
+ GATE(ZX297520V3_I2C1_WCLK, "i2c1-wclk", "i2c1-mux", 0x2c, 1, 0),
+ GATE(ZX297520V3_SPI0_PCLK, "spi0-pclk", "lsp-pclk", 0x30, 0, 0),
+ GATE(ZX297520V3_SPI0_WCLK, "spi0-wclk", "spi0-div", 0x30, 1, 0),
+ GATE(ZX297520V3_TIMER_LB_PCLK, "timer-lb-pclk", "lsp-pclk", 0x34, 0, 0),
+ GATE(ZX297520V3_TIMER_LB_WCLK, "timer-lb-wclk", "timer-lb-div", 0x34, 1, 0),
+ GATE(ZX297520V3_TIMER_LC_PCLK, "timer-lc-pclk", "lsp-pclk", 0x38, 0, 0),
+ GATE(ZX297520V3_TIMER_LC_WCLK, "timer-lc-wclk", "timer-lc-div", 0x38, 1, 0),
+ GATE(ZX297520V3_UART2_PCLK, "uart2-pclk", "lsp-pclk", 0x3c, 0, 0),
+ GATE(ZX297520V3_UART2_WCLK, "uart2-wclk", "uart2-mux", 0x3c, 1, 0),
+ GATE(ZX297520V3_WDT_LE_PCLK, "wdt-le-pclk", "lsp-pclk", 0x40, 0, 0),
+ GATE(ZX297520V3_WDT_LE_WCLK, "wdt-le-wclk", "wdt-le-div", 0x40, 1, 0),
+ GATE(ZX297520V3_TIMER_LF_PCLK, "timer-lf-pclk", "lsp-pclk", 0x44, 0, 0),
+ GATE(ZX297520V3_TIMER_LF_WCLK, "timer-lf-wclk", "timer-lf-div", 0x44, 1, 0),
+ GATE(ZX297520V3_SPI1_PCLK, "spi1-pclk", "lsp-pclk", 0x48, 0, 0),
+ GATE(ZX297520V3_SPI1_WCLK, "spi1-wclk", "spi1-div", 0x48, 1, 0),
+ GATE(ZX297520V3_TIMER_L11_PCLK, "timer-l11-pclk", "lsp-pclk", 0x4c, 0, 0),
+ GATE(ZX297520V3_TIMER_L11_WCLK, "timer-l11-wclk", "timer-l11-div", 0x4c, 1, 0),
+ GATE(ZX297520V3_TDM_PCLK, "tdm-pclk", "lsp-pclk", 0x50, 0, 0),
+ GATE(ZX297520V3_TDM_WCLK, "tdm-wclk", "tdm-div", 0x50, 1, 0),
+};
+
+static const char * const zx297520v3_lsp_inputs[] = {
+ "mpll-d5", "mpll-d4", "mpll-d6", "mpll-d8", "mpll-d12",
+ "osc26m", "osc32k", "tdm-wclk", "dpll-d4"
+};
+
+static const char * const zx297520v3_lsp_inputs_enable[] = {
+ "pclk"
+};
+
+static const struct zx_clk_data zx297520v3_lspclk_data = {
+ .inputs_enable = zx297520v3_lsp_inputs_enable,
+ .num_inputs_enable = ARRAY_SIZE(zx297520v3_lsp_inputs_enable),
+ .inputs = zx297520v3_lsp_inputs,
+ .num_inputs = ARRAY_SIZE(zx297520v3_lsp_inputs),
+ .muxes = zx297520v3_lsp_muxes,
+ .num_muxes = ARRAY_SIZE(zx297520v3_lsp_muxes),
+ .divs = zx297520v3_lsp_dividers,
+ .num_divs = ARRAY_SIZE(zx297520v3_lsp_dividers),
+ .gates = zx297520v3_lsp_gates,
+ .num_gates = ARRAY_SIZE(zx297520v3_lsp_gates),
+};
+
static int clk_zx297520v3_probe(struct platform_device *pdev)
{
const struct platform_device_id *id = platform_get_device_id(pdev);
@@ -896,6 +1081,10 @@ static const struct platform_device_id clk_zx297520v3_ids[] = {
.name = "zx297520v3-matrixclk",
.driver_data = (kernel_ulong_t)&zx297520v3_matrixclk_data,
},
+ {
+ .name = "zx297520v3-lspclk",
+ .driver_data = (kernel_ulong_t)&zx297520v3_lspclk_data,
+ },
{ }
};
MODULE_DEVICE_TABLE(platform, clk_zx297520v3_ids);
--
2.54.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* [PATCH v8 10/12] clk: zte: Introduce a driver for zx297520v3 LSP clocks
@ 2026-07-27 18:24 ` Stefan Dösinger
0 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
"LSP" is ZTE's term for this part of the SoC, I suspect it stands for
"low speed peripherals". The main UART is here, together with the flash
controller and more surplus proprietary timers.
It also has one more I2C controller that supposedly connects to a
battery charger, SPI for displays and I2S for analog telephones. The
boards I have don't have any of these components though.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
Version 8:
Double-checked pclk and wclk. They were the wrong way around in previous
submissions. Pclk is bit 0, wclk is bit 1.
Version 7:
Underscore in clock names to dashes
Version 6:
use MFD for LSP too
Version 5:
Removed unused tdm_lsp_sel. Fix zx297529 vs zx297520 typo.
Adjusted .compatible to zte,zx297520v3-lspcrm
---
drivers/clk/zte/clk-zx297520v3.c | 189 +++++++++++++++++++++++++++++++++++++++
1 file changed, 189 insertions(+)
diff --git a/drivers/clk/zte/clk-zx297520v3.c b/drivers/clk/zte/clk-zx297520v3.c
index 9299449097b4..2fe7ac389325 100644
--- a/drivers/clk/zte/clk-zx297520v3.c
+++ b/drivers/clk/zte/clk-zx297520v3.c
@@ -876,6 +876,191 @@ static const struct zx_clk_data zx297520v3_matrixclk_data = {
.num_gates = ARRAY_SIZE(zx297520v3_matrix_gates),
};
+/* LSP clock entries have a common pattern: Bit 0 for pclk, Bit 1 for wclk. Bit 4 (and sometimes
+ * more) for wclk mux.
+ *
+ * Bit 8 and 9 are resets handled by the reset-zte-zx297520v3 driver.
+ *
+ * Bits 15:12 can be a divisor, but not all clocks have it. Some clocks have a divisor in 19:16.
+ *
+ * The ID given in this table is the first register in the device's MMIO space. ZTE's drivers
+ * usually call this a version register, but it looks more like a device identifier.
+ *
+ * It looks like the registers map to devices like this:
+ *
+ * Timer reg function div dev offset(lsp + xxxx) ID
+ * 0x0: Read-only, probably device identifier 0x00752100
+ * 0x4: timer_l1 Y 0x1000 0x02020000
+ * 0x8: watchdog_l2 Y 0x2000 0x02020000
+ * 0xc: watchdog_l3 Y 0x3000 0x02020000
+ * 0x10: pwm N 0x4000 0x01020000
+ * 0x14: i2s0 Yh 0x5000 0x01030000
+ * 0x18: always 0 - - -
+ * 0x1c: i2s1 Yh 0x6000 0x01030000
+ * 0x20: always 0 - - -
+ * 0x24: qspi N 0x7000 0x01040000
+ * 0x28: uart1 N 0x8000 0x01060000
+ * 0x2c: i2c1 N 0x9000 0x01020000
+ * 0x30: spi0 Y 0xa000 0x01040000
+ * 0x34: timer_lb Y 0xb000 0x02020000
+ * 0x38: timer_lc Y 0xc000 0x02020000
+ * 0x3c: uart2 N 0xd000 0x01060000
+ * 0x40: watchdog_le Y 0xe000 0x02020000
+ * 0x44: timer_lf Y 0xf000 0x02020000
+ * 0x48: spi1 Y 0x10000 0x01040000
+ * 0x4c: timer_l11 Y 0x11000 0x02020000
+ * 0x50: tdm Yh 0x12000 0x01040000
+ *
+ * Registers 0x58, 0x5c, 0x60, 0x64, 0x68 seem to contain more controls for i2s and tdm.
+ *
+ * I am not sure about the device at offset 0x4000 (clk reg 0x10). The ID matches that of i2c, but
+ * it has a larger register set. I suspect it is a PWM device, but I have not seen any ZTE kernel
+ * operate it - even devices with displays only use a GPIO to control the backlight.
+ */
+
+static const char * const timer_lsp_sel[] = {
+ "lsp-osc32k",
+ "lsp-osc26m",
+};
+
+static const char * const uart_lsp_sel[] = {
+ "lsp-osc26m",
+ "lsp-mpll-d6",
+};
+
+static const char * const i2s_lsp_sel[] = {
+ "lsp-osc26m",
+ "lsp-dpll-d4",
+ "lsp-mpll-d6",
+ /* Unknown */
+};
+
+static const char * const spi_lsp_sel[] = {
+ "lsp-osc26m",
+ "lsp-mpll-d4",
+ "lsp-mpll-d6",
+ /* Unknown */
+};
+
+static const char * const qspi_lsp_sel[] = {
+ "lsp-osc26m",
+ "lsp-mpll-d4",
+ "lsp-mpll-d5",
+ "lsp-mpll-d6",
+ "lsp-mpll-d8",
+ "lsp-mpll-d12",
+ "lsp-osc26m",
+ "lsp-osc26m",
+};
+
+static const struct zx_mux_desc zx297520v3_lsp_muxes[] = {
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-l1-mux", timer_lsp_sel, 0x04, 4, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "wdt-l2-mux", timer_lsp_sel, 0x08, 4, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "wdt-l3-mux", timer_lsp_sel, 0x0c, 4, 1),
+ /* PWM: No mux bit can be set */
+ MUX(ZX297520V3_CLK_NO_EXPORT, "i2s0-mux", i2s_lsp_sel, 0x14, 4, 2),
+ /* 0x18: Always 0 */
+ MUX(ZX297520V3_CLK_NO_EXPORT, "i2s1-mux", i2s_lsp_sel, 0x1c, 4, 2),
+ /* 0x20: Always 0 */
+ MUX(ZX297520V3_CLK_NO_EXPORT, "qspi-mux", qspi_lsp_sel, 0x24, 4, 3),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "uart1-mux", uart_lsp_sel, 0x28, 4, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "i2c1-mux", uart_lsp_sel, 0x2c, 4, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "spi0-mux", spi_lsp_sel, 0x30, 4, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-lb-mux", timer_lsp_sel, 0x34, 4, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-lc-mux", timer_lsp_sel, 0x38, 4, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "uart2-mux", uart_lsp_sel, 0x3c, 4, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "wdt-le-mux", timer_lsp_sel, 0x40, 4, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-lf-mux", timer_lsp_sel, 0x44, 4, 1),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "spi1-mux", spi_lsp_sel, 0x48, 4, 2),
+ MUX(ZX297520V3_CLK_NO_EXPORT, "timer-l11-mux", timer_lsp_sel, 0x4c, 4, 1),
+ /* TDM: No mux in LSP. Instead, it is in matrix with a separate clk line to LSP */
+};
+
+static const struct zx_div_desc zx297520v3_lsp_dividers[] = {
+ DIV("timer-l1-div", "timer-l1-mux", 0x04, 12, 4),
+ DIV("wdt-l2-div", "wdt-l2-mux", 0x08, 12, 4),
+ DIV("wdt-l3-div", "wdt-l3-mux", 0x0c, 12, 4),
+ /* PWM: No div */
+ DIV("i2s0-div", "i2s0-mux", 0x14, 16, 4),
+ /* 0x18: Always 0 */
+ DIV("i2s1-div", "i2s1-mux", 0x1c, 16, 4),
+ /* 0x20: Always 0 */
+ /* qspi, uart1, i2c1: No div */
+ DIV("spi0-div", "spi0-mux", 0x30, 12, 4),
+ DIV("timer-lb-div", "timer-lb-mux", 0x34, 12, 4),
+ DIV("timer-lc-div", "timer-lc-mux", 0x38, 12, 4),
+ /* uart2: No div */
+ DIV("wdt-le-div", "wdt-le-mux", 0x40, 12, 4),
+ DIV("timer-lf-div", "timer-lf-mux", 0x44, 12, 4),
+ DIV("spi1-div", "spi1-mux", 0x48, 12, 4),
+ DIV("timer-l11-div", "timer-l11-mux", 0x4c, 12, 4),
+ DIV("tdm-div", "lsp-tdm-wclk", 0x50, 16, 4),
+};
+
+static const struct zx_gate_desc zx297520v3_lsp_gates[] = {
+ GATE(ZX297520V3_TIMER_L1_PCLK, "timer-l1-pclk", "lsp-pclk", 0x04, 0, 0),
+ GATE(ZX297520V3_TIMER_L1_WCLK, "timer-l1-wclk", "timer-l1-div", 0x04, 1, 0),
+ GATE(ZX297520V3_WDT_L2_PCLK, "wdt-l2-pclk", "lsp-pclk", 0x08, 0, 0),
+ GATE(ZX297520V3_WDT_L2_WCLK, "wdt-l2-wclk", "wdt-l2-div", 0x08, 1, 0),
+ GATE(ZX297520V3_WDT_L3_PCLK, "wdt-l3-pclk", "lsp-pclk", 0x0c, 0, 0),
+ GATE(ZX297520V3_WDT_L3_WCLK, "wdt-l3-wclk", "wdt-l3-div", 0x0c, 1, 0),
+ /* I don't know the LSP parent. It must be one of the LSP inputs though. */
+ GATE(ZX297520V3_PWM_PCLK, "pwm-pclk", "lsp-pclk", 0x10, 0, 0),
+ GATE(ZX297520V3_PWM_WCLK, "pwm-wclk", "lsp-osc26m", 0x10, 1, 0),
+ GATE(ZX297520V3_I2S0_PCLK, "i2s0-pclk", "lsp-pclk", 0x14, 0, 0),
+ GATE(ZX297520V3_I2S0_WCLK, "i2s0-wclk", "i2s0-div", 0x14, 1, 0),
+ /* 0x18: Always 0 */
+ GATE(ZX297520V3_I2S1_PCLK, "i2s1-pclk", "lsp-pclk", 0x1c, 0, 0),
+ GATE(ZX297520V3_I2S1_WCLK, "i2s1-wclk", "i2s1-div", 0x1c, 1, 0),
+ /* 0x20: Always 0 */
+ GATE(ZX297520V3_QSPI_PCLK, "qspi-pclk", "lsp-pclk", 0x24, 0, 0),
+ GATE(ZX297520V3_QSPI_WCLK, "qspi-wclk", "qspi-mux", 0x24, 1, 0),
+ GATE(ZX297520V3_UART1_PCLK, "uart1-pclk", "lsp-pclk", 0x28, 0, 0),
+ GATE(ZX297520V3_UART1_WCLK, "uart1-wclk", "uart1-mux", 0x28, 1, 0),
+ GATE(ZX297520V3_I2C1_PCLK, "i2c1-pclk", "lsp-pclk", 0x2c, 0, 0),
+ GATE(ZX297520V3_I2C1_WCLK, "i2c1-wclk", "i2c1-mux", 0x2c, 1, 0),
+ GATE(ZX297520V3_SPI0_PCLK, "spi0-pclk", "lsp-pclk", 0x30, 0, 0),
+ GATE(ZX297520V3_SPI0_WCLK, "spi0-wclk", "spi0-div", 0x30, 1, 0),
+ GATE(ZX297520V3_TIMER_LB_PCLK, "timer-lb-pclk", "lsp-pclk", 0x34, 0, 0),
+ GATE(ZX297520V3_TIMER_LB_WCLK, "timer-lb-wclk", "timer-lb-div", 0x34, 1, 0),
+ GATE(ZX297520V3_TIMER_LC_PCLK, "timer-lc-pclk", "lsp-pclk", 0x38, 0, 0),
+ GATE(ZX297520V3_TIMER_LC_WCLK, "timer-lc-wclk", "timer-lc-div", 0x38, 1, 0),
+ GATE(ZX297520V3_UART2_PCLK, "uart2-pclk", "lsp-pclk", 0x3c, 0, 0),
+ GATE(ZX297520V3_UART2_WCLK, "uart2-wclk", "uart2-mux", 0x3c, 1, 0),
+ GATE(ZX297520V3_WDT_LE_PCLK, "wdt-le-pclk", "lsp-pclk", 0x40, 0, 0),
+ GATE(ZX297520V3_WDT_LE_WCLK, "wdt-le-wclk", "wdt-le-div", 0x40, 1, 0),
+ GATE(ZX297520V3_TIMER_LF_PCLK, "timer-lf-pclk", "lsp-pclk", 0x44, 0, 0),
+ GATE(ZX297520V3_TIMER_LF_WCLK, "timer-lf-wclk", "timer-lf-div", 0x44, 1, 0),
+ GATE(ZX297520V3_SPI1_PCLK, "spi1-pclk", "lsp-pclk", 0x48, 0, 0),
+ GATE(ZX297520V3_SPI1_WCLK, "spi1-wclk", "spi1-div", 0x48, 1, 0),
+ GATE(ZX297520V3_TIMER_L11_PCLK, "timer-l11-pclk", "lsp-pclk", 0x4c, 0, 0),
+ GATE(ZX297520V3_TIMER_L11_WCLK, "timer-l11-wclk", "timer-l11-div", 0x4c, 1, 0),
+ GATE(ZX297520V3_TDM_PCLK, "tdm-pclk", "lsp-pclk", 0x50, 0, 0),
+ GATE(ZX297520V3_TDM_WCLK, "tdm-wclk", "tdm-div", 0x50, 1, 0),
+};
+
+static const char * const zx297520v3_lsp_inputs[] = {
+ "mpll-d5", "mpll-d4", "mpll-d6", "mpll-d8", "mpll-d12",
+ "osc26m", "osc32k", "tdm-wclk", "dpll-d4"
+};
+
+static const char * const zx297520v3_lsp_inputs_enable[] = {
+ "pclk"
+};
+
+static const struct zx_clk_data zx297520v3_lspclk_data = {
+ .inputs_enable = zx297520v3_lsp_inputs_enable,
+ .num_inputs_enable = ARRAY_SIZE(zx297520v3_lsp_inputs_enable),
+ .inputs = zx297520v3_lsp_inputs,
+ .num_inputs = ARRAY_SIZE(zx297520v3_lsp_inputs),
+ .muxes = zx297520v3_lsp_muxes,
+ .num_muxes = ARRAY_SIZE(zx297520v3_lsp_muxes),
+ .divs = zx297520v3_lsp_dividers,
+ .num_divs = ARRAY_SIZE(zx297520v3_lsp_dividers),
+ .gates = zx297520v3_lsp_gates,
+ .num_gates = ARRAY_SIZE(zx297520v3_lsp_gates),
+};
+
static int clk_zx297520v3_probe(struct platform_device *pdev)
{
const struct platform_device_id *id = platform_get_device_id(pdev);
@@ -896,6 +1081,10 @@ static const struct platform_device_id clk_zx297520v3_ids[] = {
.name = "zx297520v3-matrixclk",
.driver_data = (kernel_ulong_t)&zx297520v3_matrixclk_data,
},
+ {
+ .name = "zx297520v3-lspclk",
+ .driver_data = (kernel_ulong_t)&zx297520v3_lspclk_data,
+ },
{ }
};
MODULE_DEVICE_TABLE(platform, clk_zx297520v3_ids);
--
2.54.0
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 50+ messages in thread* Re: [PATCH v8 10/12] clk: zte: Introduce a driver for zx297520v3 LSP clocks
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 19:46 ` sashiko-bot
-1 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 19:46 UTC (permalink / raw)
To: Stefan Dösinger
Cc: conor+dt, mfd, linux-phy, lee, neil.armstrong, robh, devicetree,
vkoul, olteanv
> "LSP" is ZTE's term for this part of the SoC, I suspect it stands for
> "low speed peripherals". The main UART is here, together with the flash
> controller and more surplus proprietary timers.
>
> It also has one more I2C controller that supposedly connects to a
> battery charger, SPI for displays and I2S for analog telephones. The
> boards I have don't have any of these components though.
>
> Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=10
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 50+ messages in thread
* Re: [PATCH v8 10/12] clk: zte: Introduce a driver for zx297520v3 LSP clocks
@ 2026-07-27 19:46 ` sashiko-bot
0 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 19:46 UTC (permalink / raw)
To: Stefan Dösinger
Cc: conor+dt, mfd, linux-phy, lee, neil.armstrong, robh, devicetree,
vkoul, olteanv
> "LSP" is ZTE's term for this part of the SoC, I suspect it stands for
> "low speed peripherals". The main UART is here, together with the flash
> controller and more surplus proprietary timers.
>
> It also has one more I2C controller that supposedly connects to a
> battery charger, SPI for displays and I2S for analog telephones. The
> boards I have don't have any of these components though.
>
> Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=10
^ permalink raw reply [flat|nested] 50+ messages in thread
* [PATCH v8 11/12] reset: zte: Add a zx297520v3 reset driver
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 18:24 ` Stefan Dösinger
-1 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
This drives the MFD child devices created by the zte-zx297520v3-crm
driver.
Reviewed-by: Philipp Zabel <p.zabel@pengutronix.de>
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
v8: Select MFD_SYSCON for COMPILE_TESTing this driver without the MFD
parent driver.
v7:
Add MODULE_DEVICE_TABLE (Philipp)
Add DDR control and LSP resets
v6:
Add a comment wrt checking all bits in .status() (Philipp)
include cleanup (Philipp, Sashiko)
v5:
Make top and matrix MFD children instead of aux devices
Split USB PHY reset into its own reset ID
Remove USB reset wait code - this will be handled via syscon from a
future minimal phy-zx29-usb driver
---
MAINTAINERS | 1 +
drivers/reset/Kconfig | 12 ++
drivers/reset/Makefile | 1 +
drivers/reset/reset-zte-zx297520v3.c | 237 +++++++++++++++++++++++++++++++++++
4 files changed, 251 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 692f005cb2c4..2db95644cf98 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3885,6 +3885,7 @@ F: arch/arm/boot/dts/zte/
F: arch/arm/mach-zte/
F: drivers/clk/zte/
F: drivers/mfd/zte-zx297520v3-crm.c
+F: drivers/reset/reset-zte-zx297520v3.c
F: include/dt-bindings/clock/zte,zx297520v3-clk.h
F: include/dt-bindings/phy/zte,zx297520v3-topcrm.h
F: include/dt-bindings/reset/zte,zx297520v3-reset.h
diff --git a/drivers/reset/Kconfig b/drivers/reset/Kconfig
index d009eb0849a3..97f41574d81d 100644
--- a/drivers/reset/Kconfig
+++ b/drivers/reset/Kconfig
@@ -404,6 +404,18 @@ config RESET_UNIPHIER_GLUE
on UniPhier SoCs. Say Y if you want to control reset signals
provided by the glue layer.
+config RESET_ZTE_ZX297520V3
+ tristate "ZTE zx297520v3 Reset Driver"
+ depends on MFD_ZTE_ZX297520V3_CRM || COMPILE_TEST
+ default SOC_ZX297520V3
+ select MFD_SYSCON
+ help
+ This enables the reset controllers for ZTE zx297520v3 SoCs. The reset
+ controllers are required for booting this board and are part of the
+ clock and reset management controllers on this board, so you will also
+ need MFD_ZTE_ZX297520V3_CRM. Enable this if you are building a kernel
+ for a ZTE zx297520v3 based board.
+
config RESET_ZYNQ
bool "ZYNQ Reset Driver" if COMPILE_TEST
default ARCH_ZYNQ
diff --git a/drivers/reset/Makefile b/drivers/reset/Makefile
index 3e52569bd276..9a8a48d44dc4 100644
--- a/drivers/reset/Makefile
+++ b/drivers/reset/Makefile
@@ -50,5 +50,6 @@ obj-$(CONFIG_RESET_TI_TPS380X) += reset-tps380x.o
obj-$(CONFIG_RESET_TN48M_CPLD) += reset-tn48m.o
obj-$(CONFIG_RESET_UNIPHIER) += reset-uniphier.o
obj-$(CONFIG_RESET_UNIPHIER_GLUE) += reset-uniphier-glue.o
+obj-$(CONFIG_RESET_ZTE_ZX297520V3) += reset-zte-zx297520v3.o
obj-$(CONFIG_RESET_ZYNQ) += reset-zynq.o
obj-$(CONFIG_RESET_ZYNQMP) += reset-zynqmp.o
diff --git a/drivers/reset/reset-zte-zx297520v3.c b/drivers/reset/reset-zte-zx297520v3.c
new file mode 100644
index 000000000000..9636530e37bd
--- /dev/null
+++ b/drivers/reset/reset-zte-zx297520v3.c
@@ -0,0 +1,237 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2026 Stefan Dösinger
+ */
+#include <dt-bindings/reset/zte,zx297520v3-reset.h>
+#include <linux/reset-controller.h>
+#include <linux/platform_device.h>
+#include <linux/mfd/syscon.h>
+#include <linux/regmap.h>
+#include <linux/module.h>
+#include <linux/types.h>
+#include <linux/errno.h>
+#include <linux/bits.h>
+#include <linux/err.h>
+
+/* Most devices on the zx297520v3 SoC have one reset bit per clock line. As a rule of thumb, the
+ * lower bit disconnects the device from the bus, similarly to turning off PCLK - registers read 0
+ * or hang indefinitely. Unlike PCLK, this reset may have a lingering effect after deasserting.
+ * E.g. timers will be disabled, but retain their counter value.
+ *
+ * The other bit resets the actual device registers.
+ *
+ * For some devices, e.g. GMAC, both reset bits behave in the same way: They disconnect the device
+ * and registers will have their default state after deasserting. For devices that have two reset
+ * bits, both need to be deasserted for the device to function.
+ */
+struct zte_reset_reg {
+ u32 mask;
+ u16 reg;
+};
+
+struct zte_reset_data {
+ const struct zte_reset_reg *resets;
+ unsigned int num;
+};
+
+struct zte_reset {
+ struct reset_controller_dev rcdev;
+ struct regmap *map;
+ const struct zte_reset_reg *resets;
+};
+
+static inline struct zte_reset *to_zte_reset(struct reset_controller_dev *rcdev)
+{
+ return container_of(rcdev, struct zte_reset, rcdev);
+}
+
+static int zx29_rst_assert(struct reset_controller_dev *rcdev, unsigned long id)
+{
+ struct zte_reset *rst = to_zte_reset(rcdev);
+
+ return regmap_clear_bits(rst->map, rst->resets[id].reg, rst->resets[id].mask);
+}
+
+static int zx29_rst_deassert(struct reset_controller_dev *rcdev, unsigned long id)
+{
+ struct zte_reset *rst = to_zte_reset(rcdev);
+
+ return regmap_set_bits(rst->map, rst->resets[id].reg, rst->resets[id].mask);
+}
+
+static int zx29_rst_status(struct reset_controller_dev *rcdev, unsigned long id)
+{
+ struct zte_reset *rst = to_zte_reset(rcdev);
+ int res;
+
+ /* Devices with two reset bits need both deasserted to work. So only report them as
+ * deasserted if both bits are set.
+ *
+ * assert()/deassert() will always clear/set both. The only reason a device might be in a
+ * hybrid state is an unexpected handover state from the bootloader.
+ */
+ res = regmap_test_bits(rst->map, rst->resets[id].reg, rst->resets[id].mask);
+ if (res < 0)
+ return res;
+
+ return !res;
+}
+
+static const struct reset_control_ops zx29_rst_ops = {
+ .assert = zx29_rst_assert,
+ .deassert = zx29_rst_deassert,
+ .status = zx29_rst_status,
+};
+
+static const struct zte_reset_reg zx297520v3_top_resets[] = {
+ /* This bit is set by ZTE's cpko.ko blob, it looks like a reset bit for the LTE DSP
+ * coprocessor. Clocks for it are in matrixcrm.
+ */
+ [ZX297520V3_ZSP_RESET] = { .reg = 0x13c, .mask = BIT(0) },
+
+ [ZX297520V3_UART0_RESET] = { .reg = 0x78, .mask = BIT(6) | BIT(7) },
+ [ZX297520V3_I2C0_RESET] = { .reg = 0x74, .mask = BIT(8) | BIT(9) },
+ /* Only one reset. Bit 5 is settable but does not do anything observable */
+ [ZX297520V3_RTC_RESET] = { .reg = 0x74, .mask = BIT(4) },
+ [ZX297520V3_TIMER_T08_RESET] = { .reg = 0x78, .mask = BIT(4) | BIT(5) },
+ [ZX297520V3_TIMER_T09_RESET] = { .reg = 0x78, .mask = BIT(2) | BIT(3) },
+ /* Only one reset. Bit 0 is settable but does not do anything observable */
+ [ZX297520V3_PMM_RESET] = { .reg = 0x74, .mask = BIT(1) },
+
+ /* I haven't found any clocks for GPIO. It probably wouldn't make much
+ * sense anyway. Only one reset bit per controller.
+ */
+ [ZX297520V3_GPIO_RESET] = { .reg = 0x74, .mask = BIT(3) },
+ [ZX297520V3_GPIO8_RESET] = { .reg = 0x74, .mask = BIT(2) },
+
+ [ZX297520V3_TIMER_T12_RESET] = { .reg = 0x74, .mask = BIT(6) | BIT(7) },
+ [ZX297520V3_TIMER_T13_RESET] = { .reg = 0x7c, .mask = BIT(0) | BIT(1) },
+ [ZX297520V3_TIMER_T14_RESET] = { .reg = 0x7c, .mask = BIT(2) | BIT(3) },
+ [ZX297520V3_TIMER_T15_RESET] = { .reg = 0x74, .mask = BIT(10) | BIT(11) },
+ [ZX297520V3_TIMER_T16_RESET] = { .reg = 0x7c, .mask = BIT(4) | BIT(5) },
+ [ZX297520V3_TIMER_T17_RESET] = { .reg = 0x12c, .mask = BIT(0) | BIT(1) },
+ [ZX297520V3_WDT_T18_RESET] = { .reg = 0x74, .mask = BIT(12) | BIT(13) },
+ [ZX297520V3_USIM1_RESET] = { .reg = 0x74, .mask = BIT(14) | BIT(15) },
+ [ZX297520V3_AHB_RESET] = { .reg = 0x70, .mask = BIT(0) | BIT(1) },
+
+ /* USB reset. 0x84 returns the USB device status (0x1 for HSIC up, 0x2 for USB up, but
+ * all 3 bits (PCLK, WCLK, PHY) need to be deasserted for the device to report ready.
+ * Thus polling the status is the responsibility of the USB PHY driver.
+ */
+ [ZX297520V3_USB_PHY_RESET] = { .reg = 0x80, .mask = BIT(3) },
+ [ZX297520V3_USB_RESET] = { .reg = 0x80, .mask = BIT(4) | BIT(5) },
+ [ZX297520V3_HSIC_PHY_RESET] = { .reg = 0x80, .mask = BIT(0) },
+ [ZX297520V3_HSIC_RESET] = { .reg = 0x80, .mask = BIT(1) | BIT(2) },
+};
+
+static const struct zte_reset_data zx297520v3_topreset_data = {
+ .resets = zx297520v3_top_resets,
+ .num = ARRAY_SIZE(zx297520v3_top_resets),
+};
+
+static const struct zte_reset_reg zx297520v3_matrix_resets[] = {
+ [ZX297520V3_CPU_RESET] = { .reg = 0x28, .mask = BIT(1) },
+ [ZX297520V3_DDR_CTRL_RESET] = { .reg = 0x100, .mask = BIT(10) | BIT(11) },
+ [ZX297520V3_EDCP_RESET] = { .reg = 0x68, .mask = BIT(0) },
+ [ZX297520V3_SD0_RESET] = { .reg = 0x58, .mask = BIT(1) },
+ [ZX297520V3_SD1_RESET] = { .reg = 0x58, .mask = BIT(0) },
+ [ZX297520V3_NAND_RESET] = { .reg = 0x58, .mask = BIT(4) },
+ [ZX297520V3_PDCFG_RESET] = { .reg = 0x94, .mask = BIT(20) },
+ [ZX297520V3_SSC_RESET] = { .reg = 0x94, .mask = BIT(24) },
+ [ZX297520V3_GMAC_RESET] = { .reg = 0x114, .mask = BIT(0) | BIT(1) },
+ [ZX297520V3_VOU_RESET] = { .reg = 0x16c, .mask = BIT(0) },
+ [ZX297520V3_LSP_RESET] = { .reg = 0x80, .mask = BIT(0) },
+};
+
+static const struct zte_reset_data zx297520v3_matrixreset_data = {
+ .resets = zx297520v3_matrix_resets,
+ .num = ARRAY_SIZE(zx297520v3_matrix_resets),
+};
+
+static const struct zte_reset_reg zx297520v3_lsp_resets[] = {
+ [ZX297520V3_TIMER_L1_RESET] = { .reg = 0x04, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_WDT_L2_RESET] = { .reg = 0x08, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_WDT_L3_RESET] = { .reg = 0x0c, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_PWM_RESET] = { .reg = 0x10, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_I2S0_RESET] = { .reg = 0x14, .mask = BIT(8) | BIT(9) },
+ /* 0x18: Not writeable */
+ [ZX297520V3_I2S1_RESET] = { .reg = 0x1c, .mask = BIT(8) | BIT(9) },
+ /* 0x20: Not writeable */
+ [ZX297520V3_QSPI_RESET] = { .reg = 0x24, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_UART1_RESET] = { .reg = 0x28, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_I2C1_RESET] = { .reg = 0x2c, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_SPI0_RESET] = { .reg = 0x30, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_TIMER_LB_RESET] = { .reg = 0x34, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_TIMER_LC_RESET] = { .reg = 0x38, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_UART2_RESET] = { .reg = 0x3c, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_WDT_LE_RESET] = { .reg = 0x40, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_TIMER_LF_RESET] = { .reg = 0x44, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_SPI1_RESET] = { .reg = 0x48, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_TIMER_L11_RESET] = { .reg = 0x4c, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_TDM_RESET] = { .reg = 0x50, .mask = BIT(8) | BIT(9) },
+};
+
+static const struct zte_reset_data zx297520v3_lspreset_data = {
+ .resets = zx297520v3_lsp_resets,
+ .num = ARRAY_SIZE(zx297520v3_lsp_resets),
+};
+
+static int reset_zx297520v3_probe(struct platform_device *pdev)
+{
+ const struct platform_device_id *id = platform_get_device_id(pdev);
+ struct device *dev = &pdev->dev;
+ struct device_node *of_node = dev->parent->of_node;
+ const struct zte_reset_data *data;
+ struct zte_reset *rst;
+
+ if (!id)
+ return -ENODEV;
+ data = (const struct zte_reset_data *)id->driver_data;
+
+ rst = devm_kzalloc(dev, sizeof(*rst), GFP_KERNEL);
+ if (!rst)
+ return -ENOMEM;
+
+ rst->resets = data->resets;
+ rst->rcdev.owner = THIS_MODULE;
+ rst->rcdev.nr_resets = data->num;
+ rst->rcdev.ops = &zx29_rst_ops;
+ rst->rcdev.of_node = of_node;
+ rst->rcdev.dev = dev;
+
+ rst->map = device_node_to_regmap(of_node);
+ if (IS_ERR(rst->map))
+ return dev_err_probe(dev, PTR_ERR(rst->map), "Cannot get parent syscon regmap\n");
+
+ return devm_reset_controller_register(dev, &rst->rcdev);
+}
+
+static const struct platform_device_id reset_zx297520v3_ids[] = {
+ {
+ .name = "zx297520v3-topreset",
+ .driver_data = (kernel_ulong_t)&zx297520v3_topreset_data,
+ },
+ {
+ .name = "zx297520v3-matrixreset",
+ .driver_data = (kernel_ulong_t)&zx297520v3_matrixreset_data,
+ },
+ {
+ .name = "zx297520v3-lspreset",
+ .driver_data = (kernel_ulong_t)&zx297520v3_lspreset_data,
+ },
+ { }
+};
+MODULE_DEVICE_TABLE(platform, reset_zx297520v3_ids);
+
+static struct platform_driver reset_zx297520v3 = {
+ .probe = reset_zx297520v3_probe,
+ .driver = {
+ .name = "reset-zx297520v3",
+ },
+ .id_table = reset_zx297520v3_ids,
+};
+module_platform_driver(reset_zx297520v3);
+
+MODULE_AUTHOR("Stefan Dösinger <stefandoesinger@gmail.com>");
+MODULE_DESCRIPTION("ZTE zx297520v3 reset driver");
+MODULE_LICENSE("GPL");
--
2.54.0
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 50+ messages in thread* [PATCH v8 11/12] reset: zte: Add a zx297520v3 reset driver
@ 2026-07-27 18:24 ` Stefan Dösinger
0 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
This drives the MFD child devices created by the zte-zx297520v3-crm
driver.
Reviewed-by: Philipp Zabel <p.zabel@pengutronix.de>
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
v8: Select MFD_SYSCON for COMPILE_TESTing this driver without the MFD
parent driver.
v7:
Add MODULE_DEVICE_TABLE (Philipp)
Add DDR control and LSP resets
v6:
Add a comment wrt checking all bits in .status() (Philipp)
include cleanup (Philipp, Sashiko)
v5:
Make top and matrix MFD children instead of aux devices
Split USB PHY reset into its own reset ID
Remove USB reset wait code - this will be handled via syscon from a
future minimal phy-zx29-usb driver
---
MAINTAINERS | 1 +
drivers/reset/Kconfig | 12 ++
drivers/reset/Makefile | 1 +
drivers/reset/reset-zte-zx297520v3.c | 237 +++++++++++++++++++++++++++++++++++
4 files changed, 251 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 692f005cb2c4..2db95644cf98 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3885,6 +3885,7 @@ F: arch/arm/boot/dts/zte/
F: arch/arm/mach-zte/
F: drivers/clk/zte/
F: drivers/mfd/zte-zx297520v3-crm.c
+F: drivers/reset/reset-zte-zx297520v3.c
F: include/dt-bindings/clock/zte,zx297520v3-clk.h
F: include/dt-bindings/phy/zte,zx297520v3-topcrm.h
F: include/dt-bindings/reset/zte,zx297520v3-reset.h
diff --git a/drivers/reset/Kconfig b/drivers/reset/Kconfig
index d009eb0849a3..97f41574d81d 100644
--- a/drivers/reset/Kconfig
+++ b/drivers/reset/Kconfig
@@ -404,6 +404,18 @@ config RESET_UNIPHIER_GLUE
on UniPhier SoCs. Say Y if you want to control reset signals
provided by the glue layer.
+config RESET_ZTE_ZX297520V3
+ tristate "ZTE zx297520v3 Reset Driver"
+ depends on MFD_ZTE_ZX297520V3_CRM || COMPILE_TEST
+ default SOC_ZX297520V3
+ select MFD_SYSCON
+ help
+ This enables the reset controllers for ZTE zx297520v3 SoCs. The reset
+ controllers are required for booting this board and are part of the
+ clock and reset management controllers on this board, so you will also
+ need MFD_ZTE_ZX297520V3_CRM. Enable this if you are building a kernel
+ for a ZTE zx297520v3 based board.
+
config RESET_ZYNQ
bool "ZYNQ Reset Driver" if COMPILE_TEST
default ARCH_ZYNQ
diff --git a/drivers/reset/Makefile b/drivers/reset/Makefile
index 3e52569bd276..9a8a48d44dc4 100644
--- a/drivers/reset/Makefile
+++ b/drivers/reset/Makefile
@@ -50,5 +50,6 @@ obj-$(CONFIG_RESET_TI_TPS380X) += reset-tps380x.o
obj-$(CONFIG_RESET_TN48M_CPLD) += reset-tn48m.o
obj-$(CONFIG_RESET_UNIPHIER) += reset-uniphier.o
obj-$(CONFIG_RESET_UNIPHIER_GLUE) += reset-uniphier-glue.o
+obj-$(CONFIG_RESET_ZTE_ZX297520V3) += reset-zte-zx297520v3.o
obj-$(CONFIG_RESET_ZYNQ) += reset-zynq.o
obj-$(CONFIG_RESET_ZYNQMP) += reset-zynqmp.o
diff --git a/drivers/reset/reset-zte-zx297520v3.c b/drivers/reset/reset-zte-zx297520v3.c
new file mode 100644
index 000000000000..9636530e37bd
--- /dev/null
+++ b/drivers/reset/reset-zte-zx297520v3.c
@@ -0,0 +1,237 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2026 Stefan Dösinger
+ */
+#include <dt-bindings/reset/zte,zx297520v3-reset.h>
+#include <linux/reset-controller.h>
+#include <linux/platform_device.h>
+#include <linux/mfd/syscon.h>
+#include <linux/regmap.h>
+#include <linux/module.h>
+#include <linux/types.h>
+#include <linux/errno.h>
+#include <linux/bits.h>
+#include <linux/err.h>
+
+/* Most devices on the zx297520v3 SoC have one reset bit per clock line. As a rule of thumb, the
+ * lower bit disconnects the device from the bus, similarly to turning off PCLK - registers read 0
+ * or hang indefinitely. Unlike PCLK, this reset may have a lingering effect after deasserting.
+ * E.g. timers will be disabled, but retain their counter value.
+ *
+ * The other bit resets the actual device registers.
+ *
+ * For some devices, e.g. GMAC, both reset bits behave in the same way: They disconnect the device
+ * and registers will have their default state after deasserting. For devices that have two reset
+ * bits, both need to be deasserted for the device to function.
+ */
+struct zte_reset_reg {
+ u32 mask;
+ u16 reg;
+};
+
+struct zte_reset_data {
+ const struct zte_reset_reg *resets;
+ unsigned int num;
+};
+
+struct zte_reset {
+ struct reset_controller_dev rcdev;
+ struct regmap *map;
+ const struct zte_reset_reg *resets;
+};
+
+static inline struct zte_reset *to_zte_reset(struct reset_controller_dev *rcdev)
+{
+ return container_of(rcdev, struct zte_reset, rcdev);
+}
+
+static int zx29_rst_assert(struct reset_controller_dev *rcdev, unsigned long id)
+{
+ struct zte_reset *rst = to_zte_reset(rcdev);
+
+ return regmap_clear_bits(rst->map, rst->resets[id].reg, rst->resets[id].mask);
+}
+
+static int zx29_rst_deassert(struct reset_controller_dev *rcdev, unsigned long id)
+{
+ struct zte_reset *rst = to_zte_reset(rcdev);
+
+ return regmap_set_bits(rst->map, rst->resets[id].reg, rst->resets[id].mask);
+}
+
+static int zx29_rst_status(struct reset_controller_dev *rcdev, unsigned long id)
+{
+ struct zte_reset *rst = to_zte_reset(rcdev);
+ int res;
+
+ /* Devices with two reset bits need both deasserted to work. So only report them as
+ * deasserted if both bits are set.
+ *
+ * assert()/deassert() will always clear/set both. The only reason a device might be in a
+ * hybrid state is an unexpected handover state from the bootloader.
+ */
+ res = regmap_test_bits(rst->map, rst->resets[id].reg, rst->resets[id].mask);
+ if (res < 0)
+ return res;
+
+ return !res;
+}
+
+static const struct reset_control_ops zx29_rst_ops = {
+ .assert = zx29_rst_assert,
+ .deassert = zx29_rst_deassert,
+ .status = zx29_rst_status,
+};
+
+static const struct zte_reset_reg zx297520v3_top_resets[] = {
+ /* This bit is set by ZTE's cpko.ko blob, it looks like a reset bit for the LTE DSP
+ * coprocessor. Clocks for it are in matrixcrm.
+ */
+ [ZX297520V3_ZSP_RESET] = { .reg = 0x13c, .mask = BIT(0) },
+
+ [ZX297520V3_UART0_RESET] = { .reg = 0x78, .mask = BIT(6) | BIT(7) },
+ [ZX297520V3_I2C0_RESET] = { .reg = 0x74, .mask = BIT(8) | BIT(9) },
+ /* Only one reset. Bit 5 is settable but does not do anything observable */
+ [ZX297520V3_RTC_RESET] = { .reg = 0x74, .mask = BIT(4) },
+ [ZX297520V3_TIMER_T08_RESET] = { .reg = 0x78, .mask = BIT(4) | BIT(5) },
+ [ZX297520V3_TIMER_T09_RESET] = { .reg = 0x78, .mask = BIT(2) | BIT(3) },
+ /* Only one reset. Bit 0 is settable but does not do anything observable */
+ [ZX297520V3_PMM_RESET] = { .reg = 0x74, .mask = BIT(1) },
+
+ /* I haven't found any clocks for GPIO. It probably wouldn't make much
+ * sense anyway. Only one reset bit per controller.
+ */
+ [ZX297520V3_GPIO_RESET] = { .reg = 0x74, .mask = BIT(3) },
+ [ZX297520V3_GPIO8_RESET] = { .reg = 0x74, .mask = BIT(2) },
+
+ [ZX297520V3_TIMER_T12_RESET] = { .reg = 0x74, .mask = BIT(6) | BIT(7) },
+ [ZX297520V3_TIMER_T13_RESET] = { .reg = 0x7c, .mask = BIT(0) | BIT(1) },
+ [ZX297520V3_TIMER_T14_RESET] = { .reg = 0x7c, .mask = BIT(2) | BIT(3) },
+ [ZX297520V3_TIMER_T15_RESET] = { .reg = 0x74, .mask = BIT(10) | BIT(11) },
+ [ZX297520V3_TIMER_T16_RESET] = { .reg = 0x7c, .mask = BIT(4) | BIT(5) },
+ [ZX297520V3_TIMER_T17_RESET] = { .reg = 0x12c, .mask = BIT(0) | BIT(1) },
+ [ZX297520V3_WDT_T18_RESET] = { .reg = 0x74, .mask = BIT(12) | BIT(13) },
+ [ZX297520V3_USIM1_RESET] = { .reg = 0x74, .mask = BIT(14) | BIT(15) },
+ [ZX297520V3_AHB_RESET] = { .reg = 0x70, .mask = BIT(0) | BIT(1) },
+
+ /* USB reset. 0x84 returns the USB device status (0x1 for HSIC up, 0x2 for USB up, but
+ * all 3 bits (PCLK, WCLK, PHY) need to be deasserted for the device to report ready.
+ * Thus polling the status is the responsibility of the USB PHY driver.
+ */
+ [ZX297520V3_USB_PHY_RESET] = { .reg = 0x80, .mask = BIT(3) },
+ [ZX297520V3_USB_RESET] = { .reg = 0x80, .mask = BIT(4) | BIT(5) },
+ [ZX297520V3_HSIC_PHY_RESET] = { .reg = 0x80, .mask = BIT(0) },
+ [ZX297520V3_HSIC_RESET] = { .reg = 0x80, .mask = BIT(1) | BIT(2) },
+};
+
+static const struct zte_reset_data zx297520v3_topreset_data = {
+ .resets = zx297520v3_top_resets,
+ .num = ARRAY_SIZE(zx297520v3_top_resets),
+};
+
+static const struct zte_reset_reg zx297520v3_matrix_resets[] = {
+ [ZX297520V3_CPU_RESET] = { .reg = 0x28, .mask = BIT(1) },
+ [ZX297520V3_DDR_CTRL_RESET] = { .reg = 0x100, .mask = BIT(10) | BIT(11) },
+ [ZX297520V3_EDCP_RESET] = { .reg = 0x68, .mask = BIT(0) },
+ [ZX297520V3_SD0_RESET] = { .reg = 0x58, .mask = BIT(1) },
+ [ZX297520V3_SD1_RESET] = { .reg = 0x58, .mask = BIT(0) },
+ [ZX297520V3_NAND_RESET] = { .reg = 0x58, .mask = BIT(4) },
+ [ZX297520V3_PDCFG_RESET] = { .reg = 0x94, .mask = BIT(20) },
+ [ZX297520V3_SSC_RESET] = { .reg = 0x94, .mask = BIT(24) },
+ [ZX297520V3_GMAC_RESET] = { .reg = 0x114, .mask = BIT(0) | BIT(1) },
+ [ZX297520V3_VOU_RESET] = { .reg = 0x16c, .mask = BIT(0) },
+ [ZX297520V3_LSP_RESET] = { .reg = 0x80, .mask = BIT(0) },
+};
+
+static const struct zte_reset_data zx297520v3_matrixreset_data = {
+ .resets = zx297520v3_matrix_resets,
+ .num = ARRAY_SIZE(zx297520v3_matrix_resets),
+};
+
+static const struct zte_reset_reg zx297520v3_lsp_resets[] = {
+ [ZX297520V3_TIMER_L1_RESET] = { .reg = 0x04, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_WDT_L2_RESET] = { .reg = 0x08, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_WDT_L3_RESET] = { .reg = 0x0c, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_PWM_RESET] = { .reg = 0x10, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_I2S0_RESET] = { .reg = 0x14, .mask = BIT(8) | BIT(9) },
+ /* 0x18: Not writeable */
+ [ZX297520V3_I2S1_RESET] = { .reg = 0x1c, .mask = BIT(8) | BIT(9) },
+ /* 0x20: Not writeable */
+ [ZX297520V3_QSPI_RESET] = { .reg = 0x24, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_UART1_RESET] = { .reg = 0x28, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_I2C1_RESET] = { .reg = 0x2c, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_SPI0_RESET] = { .reg = 0x30, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_TIMER_LB_RESET] = { .reg = 0x34, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_TIMER_LC_RESET] = { .reg = 0x38, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_UART2_RESET] = { .reg = 0x3c, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_WDT_LE_RESET] = { .reg = 0x40, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_TIMER_LF_RESET] = { .reg = 0x44, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_SPI1_RESET] = { .reg = 0x48, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_TIMER_L11_RESET] = { .reg = 0x4c, .mask = BIT(8) | BIT(9) },
+ [ZX297520V3_TDM_RESET] = { .reg = 0x50, .mask = BIT(8) | BIT(9) },
+};
+
+static const struct zte_reset_data zx297520v3_lspreset_data = {
+ .resets = zx297520v3_lsp_resets,
+ .num = ARRAY_SIZE(zx297520v3_lsp_resets),
+};
+
+static int reset_zx297520v3_probe(struct platform_device *pdev)
+{
+ const struct platform_device_id *id = platform_get_device_id(pdev);
+ struct device *dev = &pdev->dev;
+ struct device_node *of_node = dev->parent->of_node;
+ const struct zte_reset_data *data;
+ struct zte_reset *rst;
+
+ if (!id)
+ return -ENODEV;
+ data = (const struct zte_reset_data *)id->driver_data;
+
+ rst = devm_kzalloc(dev, sizeof(*rst), GFP_KERNEL);
+ if (!rst)
+ return -ENOMEM;
+
+ rst->resets = data->resets;
+ rst->rcdev.owner = THIS_MODULE;
+ rst->rcdev.nr_resets = data->num;
+ rst->rcdev.ops = &zx29_rst_ops;
+ rst->rcdev.of_node = of_node;
+ rst->rcdev.dev = dev;
+
+ rst->map = device_node_to_regmap(of_node);
+ if (IS_ERR(rst->map))
+ return dev_err_probe(dev, PTR_ERR(rst->map), "Cannot get parent syscon regmap\n");
+
+ return devm_reset_controller_register(dev, &rst->rcdev);
+}
+
+static const struct platform_device_id reset_zx297520v3_ids[] = {
+ {
+ .name = "zx297520v3-topreset",
+ .driver_data = (kernel_ulong_t)&zx297520v3_topreset_data,
+ },
+ {
+ .name = "zx297520v3-matrixreset",
+ .driver_data = (kernel_ulong_t)&zx297520v3_matrixreset_data,
+ },
+ {
+ .name = "zx297520v3-lspreset",
+ .driver_data = (kernel_ulong_t)&zx297520v3_lspreset_data,
+ },
+ { }
+};
+MODULE_DEVICE_TABLE(platform, reset_zx297520v3_ids);
+
+static struct platform_driver reset_zx297520v3 = {
+ .probe = reset_zx297520v3_probe,
+ .driver = {
+ .name = "reset-zx297520v3",
+ },
+ .id_table = reset_zx297520v3_ids,
+};
+module_platform_driver(reset_zx297520v3);
+
+MODULE_AUTHOR("Stefan Dösinger <stefandoesinger@gmail.com>");
+MODULE_DESCRIPTION("ZTE zx297520v3 reset driver");
+MODULE_LICENSE("GPL");
--
2.54.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* Re: [PATCH v8 11/12] reset: zte: Add a zx297520v3 reset driver
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 19:59 ` sashiko-bot
-1 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 19:59 UTC (permalink / raw)
To: Stefan Dösinger
Cc: mfd, robh, neil.armstrong, vkoul, olteanv, devicetree, linux-phy,
lee, conor+dt
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The Kconfig entry selects MFD_SYSCON without inheriting its HAS_IOMEM dependency, risking an unmet direct dependency warning.
--
--- Patch [11]: [PATCH v8 11/12] reset: zte: Add a zx297520v3 reset driver ---
commit 12743a130823c55b091e1dd6972558ed49c19d61
Author: Stefan Dösinger <stefandoesinger@gmail.com>
reset: zte: Add a zx297520v3 reset driver
This commit adds the reset driver for ZTE zx297520v3 SoCs to drive the MFD
child devices created by the zte-zx297520v3-crm driver.
> diff --git a/drivers/reset/Kconfig b/drivers/reset/Kconfig
> index d009eb0849a3b..97f41574d81d5 100644
> --- a/drivers/reset/Kconfig
> +++ b/drivers/reset/Kconfig
[ ... ]
> +config RESET_ZTE_ZX297520V3
> + tristate "ZTE zx297520v3 Reset Driver"
> + depends on MFD_ZTE_ZX297520V3_CRM || COMPILE_TEST
> + default SOC_ZX297520V3
> + select MFD_SYSCON
[Severity: Medium]
Could this select statement trigger an unmet direct dependency warning?
Since this driver can be built with COMPILE_TEST, it might be compiled on
architectures like UML that do not enable HAS_IOMEM. MFD_SYSCON requires
HAS_IOMEM, so selecting it here without inheriting the HAS_IOMEM dependency
can cause Kconfig warnings and potential build failures.
Would it be better to add a dependency on HAS_IOMEM?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=11
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 50+ messages in thread
* Re: [PATCH v8 11/12] reset: zte: Add a zx297520v3 reset driver
@ 2026-07-27 19:59 ` sashiko-bot
0 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 19:59 UTC (permalink / raw)
To: Stefan Dösinger
Cc: mfd, robh, neil.armstrong, vkoul, olteanv, devicetree, linux-phy,
lee, conor+dt
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The Kconfig entry selects MFD_SYSCON without inheriting its HAS_IOMEM dependency, risking an unmet direct dependency warning.
--
--- Patch [11]: [PATCH v8 11/12] reset: zte: Add a zx297520v3 reset driver ---
commit 12743a130823c55b091e1dd6972558ed49c19d61
Author: Stefan Dösinger <stefandoesinger@gmail.com>
reset: zte: Add a zx297520v3 reset driver
This commit adds the reset driver for ZTE zx297520v3 SoCs to drive the MFD
child devices created by the zte-zx297520v3-crm driver.
> diff --git a/drivers/reset/Kconfig b/drivers/reset/Kconfig
> index d009eb0849a3b..97f41574d81d5 100644
> --- a/drivers/reset/Kconfig
> +++ b/drivers/reset/Kconfig
[ ... ]
> +config RESET_ZTE_ZX297520V3
> + tristate "ZTE zx297520v3 Reset Driver"
> + depends on MFD_ZTE_ZX297520V3_CRM || COMPILE_TEST
> + default SOC_ZX297520V3
> + select MFD_SYSCON
[Severity: Medium]
Could this select statement trigger an unmet direct dependency warning?
Since this driver can be built with COMPILE_TEST, it might be compiled on
architectures like UML that do not enable HAS_IOMEM. MFD_SYSCON requires
HAS_IOMEM, so selecting it here without inheriting the HAS_IOMEM dependency
can cause Kconfig warnings and potential build failures.
Would it be better to add a dependency on HAS_IOMEM?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=11
^ permalink raw reply [flat|nested] 50+ messages in thread
* [PATCH v8 12/12] ARM: dts: zte: Declare zx297520v3 CRM device nodes
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 18:24 ` Stefan Dösinger
-1 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
This makes use of the driver added in the previous patches. It wires up
the uart clocks and resets and allows getting rid of the placeholder
uartclk node.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
Version 7:
Add phy nodes to topcrm
Remove syscon from matrixclk again
Adapt remove now-removed extra clk inputs from matrix
Add reset to LSP
osc26m -> clock-26m for the fixed rate clocks, likewise for 32k
Version 6:
Add syscon to matrixclk
Version 5:
Re-name from *clk to *crm
Add the syscon-reboot node here because the binding requires it
Re-add accidentally dropped uart2 IRQ
---
arch/arm/boot/dts/zte/zx297520v3.dtsi | 84 +++++++++++++++++++++++++++++++----
1 file changed, 76 insertions(+), 8 deletions(-)
diff --git a/arch/arm/boot/dts/zte/zx297520v3.dtsi b/arch/arm/boot/dts/zte/zx297520v3.dtsi
index a16c30a164bb..8d911795936b 100644
--- a/arch/arm/boot/dts/zte/zx297520v3.dtsi
+++ b/arch/arm/boot/dts/zte/zx297520v3.dtsi
@@ -4,6 +4,8 @@
*/
#include <dt-bindings/interrupt-controller/arm-gic.h>
+#include <dt-bindings/reset/zte,zx297520v3-reset.h>
+#include <dt-bindings/clock/zte,zx297520v3-clk.h>
/ {
#address-cells = <1>;
@@ -20,13 +22,16 @@ cpu@0 {
};
};
- /* Base bus clock and default for the UART. It will be replaced once a clock driver has
- * been added.
- */
- uartclk: uartclk-26000000 {
- #clock-cells = <0>;
+ osc26m: clock-26m {
compatible = "fixed-clock";
clock-frequency = <26000000>;
+ #clock-cells = <0>;
+ };
+
+ osc32k: clock-32k {
+ compatible = "fixed-clock";
+ clock-frequency = <32768>;
+ #clock-cells = <0>;
};
timer {
@@ -70,13 +75,74 @@ gic: interrupt-controller@f2000000 {
<0xf2040000 0x20000>;
};
+ topcrm: clock-controller@13b000 {
+ compatible = "zte,zx297520v3-topcrm", "syscon";
+ reg = <0x0013b000 0x400>;
+ clocks = <&osc26m>, <&osc32k>;
+ clock-names = "osc26m", "osc32k";
+ interrupts = <GIC_SPI 42 IRQ_TYPE_EDGE_RISING>,
+ <GIC_SPI 43 IRQ_TYPE_EDGE_RISING>,
+ <GIC_SPI 45 IRQ_TYPE_EDGE_RISING>,
+ <GIC_SPI 46 IRQ_TYPE_EDGE_RISING>;
+ interrupt-names = "usb-up", "usb-down", "hsic-up", "hsic-down";
+ resets = <&topcrm ZX297520V3_USB_PHY_RESET>,
+ <&topcrm ZX297520V3_HSIC_PHY_RESET>;
+ reset-names = "usb", "hsic";
+ #clock-cells = <1>;
+ #phy-cells = <1>;
+ #reset-cells = <1>;
+
+ syscon-reboot {
+ compatible = "syscon-reboot";
+ offset = <0x0>;
+ value = <0x1>;
+ mask = <0x1>;
+ };
+ };
+
+ matrixcrm: clock-controller@1306000 {
+ compatible = "zte,zx297520v3-matrixcrm";
+ reg = <0x01306000 0x400>;
+ clocks = <&osc26m>, <&osc32k>,
+ <&topcrm ZX297520V3_MPLL>, <&topcrm ZX297520V3_MPLL_D5>,
+ <&topcrm ZX297520V3_DPLL>, <&topcrm ZX297520V3_GPLL>,
+ <&topcrm ZX297520V3_GPLL_D2>, <&topcrm ZX297520V3_GATED_OSC26M>;
+ clock-names = "osc26m", "osc32k", "mpll", "mpll-d5", "dpll",
+ "gpll", "gpll-d2", "wclk-osc26m";
+ #clock-cells = <1>;
+ #hwlock-cells = <1>;
+ #reset-cells = <1>;
+ };
+
+ lspcrm: clock-controller@1400000 {
+ compatible = "zte,zx297520v3-lspcrm";
+ reg = <0x01400000 0x100>;
+ #clock-cells = <1>;
+ #reset-cells = <1>;
+
+ clocks = <&matrixcrm ZX297520V3_LSP_MPLL_D5_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_MPLL_D4_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_MPLL_D6_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_MPLL_D8_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_MPLL_D12_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_OSC26M_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_OSC32K_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_PCLK>,
+ <&matrixcrm ZX297520V3_LSP_TDM_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_DPLL_D4_WCLK>;
+ clock-names = "mpll-d5", "mpll-d4", "mpll-d6", "mpll-d8", "mpll-d12",
+ "osc26m", "osc32k", "pclk", "tdm-wclk", "dpll-d4";
+ resets = <&matrixcrm ZX297520V3_LSP_RESET>;
+ };
+
uart0: serial@131000 {
compatible = "arm,pl011", "arm,primecell";
arm,primecell-periphid = <0x0018c011>;
reg = <0x00131000 0x1000>;
interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>;
- clocks = <&uartclk>, <&uartclk>;
+ clocks = <&topcrm ZX297520V3_UART0_WCLK>, <&topcrm ZX297520V3_UART0_PCLK>;
clock-names = "uartclk", "apb_pclk";
+ resets = <&topcrm ZX297520V3_UART0_RESET>;
status = "disabled";
};
@@ -85,8 +151,9 @@ uart1: serial@1408000 {
arm,primecell-periphid = <0x0018c011>;
reg = <0x01408000 0x1000>;
interrupts = <GIC_SPI 1 IRQ_TYPE_LEVEL_HIGH>;
- clocks = <&uartclk>, <&uartclk>;
+ clocks = <&lspcrm ZX297520V3_UART1_WCLK>, <&lspcrm ZX297520V3_UART1_PCLK>;
clock-names = "uartclk", "apb_pclk";
+ resets = <&lspcrm ZX297520V3_UART1_RESET>;
status = "disabled";
};
@@ -95,8 +162,9 @@ uart2: serial@140d000 {
arm,primecell-periphid = <0x0018c011>;
reg = <0x0140d000 0x1000>;
interrupts = <GIC_SPI 2 IRQ_TYPE_LEVEL_HIGH>;
- clocks = <&uartclk>, <&uartclk>;
+ clocks = <&lspcrm ZX297520V3_UART2_WCLK>, <&lspcrm ZX297520V3_UART2_PCLK>;
clock-names = "uartclk", "apb_pclk";
+ resets = <&lspcrm ZX297520V3_UART2_RESET>;
status = "disabled";
};
};
--
2.54.0
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 50+ messages in thread* [PATCH v8 12/12] ARM: dts: zte: Declare zx297520v3 CRM device nodes
@ 2026-07-27 18:24 ` Stefan Dösinger
0 siblings, 0 replies; 50+ messages in thread
From: Stefan Dösinger @ 2026-07-27 18:24 UTC (permalink / raw)
To: Michael Turquette, Stephen Boyd, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Brian Masney, Vinod Koul,
Neil Armstrong, Russell King, Lee Jones
Cc: linux-clk, devicetree, linux-kernel, linux-arm-kernel, linux-phy,
mfd, Stefan Dösinger
This makes use of the driver added in the previous patches. It wires up
the uart clocks and resets and allows getting rid of the placeholder
uartclk node.
Signed-off-by: Stefan Dösinger <stefandoesinger@gmail.com>
---
Version 7:
Add phy nodes to topcrm
Remove syscon from matrixclk again
Adapt remove now-removed extra clk inputs from matrix
Add reset to LSP
osc26m -> clock-26m for the fixed rate clocks, likewise for 32k
Version 6:
Add syscon to matrixclk
Version 5:
Re-name from *clk to *crm
Add the syscon-reboot node here because the binding requires it
Re-add accidentally dropped uart2 IRQ
---
arch/arm/boot/dts/zte/zx297520v3.dtsi | 84 +++++++++++++++++++++++++++++++----
1 file changed, 76 insertions(+), 8 deletions(-)
diff --git a/arch/arm/boot/dts/zte/zx297520v3.dtsi b/arch/arm/boot/dts/zte/zx297520v3.dtsi
index a16c30a164bb..8d911795936b 100644
--- a/arch/arm/boot/dts/zte/zx297520v3.dtsi
+++ b/arch/arm/boot/dts/zte/zx297520v3.dtsi
@@ -4,6 +4,8 @@
*/
#include <dt-bindings/interrupt-controller/arm-gic.h>
+#include <dt-bindings/reset/zte,zx297520v3-reset.h>
+#include <dt-bindings/clock/zte,zx297520v3-clk.h>
/ {
#address-cells = <1>;
@@ -20,13 +22,16 @@ cpu@0 {
};
};
- /* Base bus clock and default for the UART. It will be replaced once a clock driver has
- * been added.
- */
- uartclk: uartclk-26000000 {
- #clock-cells = <0>;
+ osc26m: clock-26m {
compatible = "fixed-clock";
clock-frequency = <26000000>;
+ #clock-cells = <0>;
+ };
+
+ osc32k: clock-32k {
+ compatible = "fixed-clock";
+ clock-frequency = <32768>;
+ #clock-cells = <0>;
};
timer {
@@ -70,13 +75,74 @@ gic: interrupt-controller@f2000000 {
<0xf2040000 0x20000>;
};
+ topcrm: clock-controller@13b000 {
+ compatible = "zte,zx297520v3-topcrm", "syscon";
+ reg = <0x0013b000 0x400>;
+ clocks = <&osc26m>, <&osc32k>;
+ clock-names = "osc26m", "osc32k";
+ interrupts = <GIC_SPI 42 IRQ_TYPE_EDGE_RISING>,
+ <GIC_SPI 43 IRQ_TYPE_EDGE_RISING>,
+ <GIC_SPI 45 IRQ_TYPE_EDGE_RISING>,
+ <GIC_SPI 46 IRQ_TYPE_EDGE_RISING>;
+ interrupt-names = "usb-up", "usb-down", "hsic-up", "hsic-down";
+ resets = <&topcrm ZX297520V3_USB_PHY_RESET>,
+ <&topcrm ZX297520V3_HSIC_PHY_RESET>;
+ reset-names = "usb", "hsic";
+ #clock-cells = <1>;
+ #phy-cells = <1>;
+ #reset-cells = <1>;
+
+ syscon-reboot {
+ compatible = "syscon-reboot";
+ offset = <0x0>;
+ value = <0x1>;
+ mask = <0x1>;
+ };
+ };
+
+ matrixcrm: clock-controller@1306000 {
+ compatible = "zte,zx297520v3-matrixcrm";
+ reg = <0x01306000 0x400>;
+ clocks = <&osc26m>, <&osc32k>,
+ <&topcrm ZX297520V3_MPLL>, <&topcrm ZX297520V3_MPLL_D5>,
+ <&topcrm ZX297520V3_DPLL>, <&topcrm ZX297520V3_GPLL>,
+ <&topcrm ZX297520V3_GPLL_D2>, <&topcrm ZX297520V3_GATED_OSC26M>;
+ clock-names = "osc26m", "osc32k", "mpll", "mpll-d5", "dpll",
+ "gpll", "gpll-d2", "wclk-osc26m";
+ #clock-cells = <1>;
+ #hwlock-cells = <1>;
+ #reset-cells = <1>;
+ };
+
+ lspcrm: clock-controller@1400000 {
+ compatible = "zte,zx297520v3-lspcrm";
+ reg = <0x01400000 0x100>;
+ #clock-cells = <1>;
+ #reset-cells = <1>;
+
+ clocks = <&matrixcrm ZX297520V3_LSP_MPLL_D5_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_MPLL_D4_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_MPLL_D6_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_MPLL_D8_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_MPLL_D12_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_OSC26M_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_OSC32K_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_PCLK>,
+ <&matrixcrm ZX297520V3_LSP_TDM_WCLK>,
+ <&matrixcrm ZX297520V3_LSP_DPLL_D4_WCLK>;
+ clock-names = "mpll-d5", "mpll-d4", "mpll-d6", "mpll-d8", "mpll-d12",
+ "osc26m", "osc32k", "pclk", "tdm-wclk", "dpll-d4";
+ resets = <&matrixcrm ZX297520V3_LSP_RESET>;
+ };
+
uart0: serial@131000 {
compatible = "arm,pl011", "arm,primecell";
arm,primecell-periphid = <0x0018c011>;
reg = <0x00131000 0x1000>;
interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>;
- clocks = <&uartclk>, <&uartclk>;
+ clocks = <&topcrm ZX297520V3_UART0_WCLK>, <&topcrm ZX297520V3_UART0_PCLK>;
clock-names = "uartclk", "apb_pclk";
+ resets = <&topcrm ZX297520V3_UART0_RESET>;
status = "disabled";
};
@@ -85,8 +151,9 @@ uart1: serial@1408000 {
arm,primecell-periphid = <0x0018c011>;
reg = <0x01408000 0x1000>;
interrupts = <GIC_SPI 1 IRQ_TYPE_LEVEL_HIGH>;
- clocks = <&uartclk>, <&uartclk>;
+ clocks = <&lspcrm ZX297520V3_UART1_WCLK>, <&lspcrm ZX297520V3_UART1_PCLK>;
clock-names = "uartclk", "apb_pclk";
+ resets = <&lspcrm ZX297520V3_UART1_RESET>;
status = "disabled";
};
@@ -95,8 +162,9 @@ uart2: serial@140d000 {
arm,primecell-periphid = <0x0018c011>;
reg = <0x0140d000 0x1000>;
interrupts = <GIC_SPI 2 IRQ_TYPE_LEVEL_HIGH>;
- clocks = <&uartclk>, <&uartclk>;
+ clocks = <&lspcrm ZX297520V3_UART2_WCLK>, <&lspcrm ZX297520V3_UART2_PCLK>;
clock-names = "uartclk", "apb_pclk";
+ resets = <&lspcrm ZX297520V3_UART2_RESET>;
status = "disabled";
};
};
--
2.54.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* Re: [PATCH v8 12/12] ARM: dts: zte: Declare zx297520v3 CRM device nodes
2026-07-27 18:24 ` Stefan Dösinger
@ 2026-07-27 20:13 ` sashiko-bot
-1 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 20:13 UTC (permalink / raw)
To: Stefan Dösinger
Cc: vkoul, neil.armstrong, linux-phy, mfd, lee, robh, olteanv,
devicetree, conor+dt
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] The lspcrm device defines a hardware reset dependency on matrixcrm (ZX297520V3_LSP_RESET), but the CRM drivers never request or deassert it, which can lead to a hardware hang when accessing the registers.
- [Low] Nodes under the `soc` bus are not sorted by their unit address.
- [Low] Non-generic node name `syscon-reboot`.
--
--- Patch [12]: [PATCH v8 12/12] ARM: dts: zte: Declare zx297520v3 CRM device nodes ---
commit ae42492b8ef6fce4731646f6907ba63dc94e9ffa
Author: Stefan Dösinger <stefandoesinger@gmail.com>
ARM: dts: zte: Declare zx297520v3 CRM device nodes
This commit wires up the zx297520v3 CRM device nodes, clock inputs,
and resets, getting rid of the placeholder uartclk node.
> diff --git a/arch/arm/boot/dts/zte/zx297520v3.dtsi b/arch/arm/boot/dts/zte/zx297520v3.dtsi
> --- a/arch/arm/boot/dts/zte/zx297520v3.dtsi
> +++ b/arch/arm/boot/dts/zte/zx297520v3.dtsi
[ ... ]
> @@ -70,13 +75,74 @@ gic: interrupt-controller@f2000000 {
> <0xf2040000 0x20000>;
> };
>
> + topcrm: clock-controller@13b000 {
> + compatible = "zte,zx297520v3-topcrm", "syscon";
> + reg = <0x0013b000 0x400>;
[Severity: Low]
Are these new nodes inserted out of order relative to their unit addresses?
The topcrm node at 13b000 is placed before matrixcrm at 1306000, and lspcrm
at 1400000 is placed before uart0 at 131000. It is customary to sort nodes
under a bus by their unit address to maintain readability.
[ ... ]
> + #clock-cells = <1>;
> + #phy-cells = <1>;
> + #reset-cells = <1>;
> +
> + syscon-reboot {
> + compatible = "syscon-reboot";
> + offset = <0x0>;
> + value = <0x1>;
> + mask = <0x1>;
> + };
[Severity: Low]
Should this node use a generic name like reboot instead of syscon-reboot?
The Devicetree specification usually recommends node names to reflect the
function of the device rather than its programming model or driver name.
> + };
> +
> + matrixcrm: clock-controller@1306000 {
> + compatible = "zte,zx297520v3-matrixcrm";
> + reg = <0x01306000 0x400>;
[ ... ]
> + lspcrm: clock-controller@1400000 {
> + compatible = "zte,zx297520v3-lspcrm";
> + reg = <0x01400000 0x100>;
> + #clock-cells = <1>;
> + #reset-cells = <1>;
[ ... ]
> + clock-names = "mpll-d5", "mpll-d4", "mpll-d6", "mpll-d8", "mpll-d12",
> + "osc26m", "osc32k", "pclk", "tdm-wclk", "dpll-d4";
> + resets = <&matrixcrm ZX297520V3_LSP_RESET>;
> + };
[Severity: High]
Does this hardware reset dependency require explicit handling in the driver?
The DTS correctly defines the dependency, but looking at zx_clk_common_probe()
in drivers/clk/zte/clk-zx.c, the regmap is obtained using:
drivers/clk/zte/clk-zx.c:zx_clk_common_probe() {
...
map = device_node_to_regmap(of_node);
if (IS_ERR(map))
return PTR_ERR(map);
...
}
Because the lspcrm node lacks the syscon compatible string, this bypasses the
generic syscon reset handling. As neither the MFD driver nor child drivers
manually deassert this reset, won't subsequent register accesses hit a reset
block and potentially hang the hardware?
> +
> uart0: serial@131000 {
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=12
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 50+ messages in thread* Re: [PATCH v8 12/12] ARM: dts: zte: Declare zx297520v3 CRM device nodes
@ 2026-07-27 20:13 ` sashiko-bot
0 siblings, 0 replies; 50+ messages in thread
From: sashiko-bot @ 2026-07-27 20:13 UTC (permalink / raw)
To: Stefan Dösinger
Cc: vkoul, neil.armstrong, linux-phy, mfd, lee, robh, olteanv,
devicetree, conor+dt
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] The lspcrm device defines a hardware reset dependency on matrixcrm (ZX297520V3_LSP_RESET), but the CRM drivers never request or deassert it, which can lead to a hardware hang when accessing the registers.
- [Low] Nodes under the `soc` bus are not sorted by their unit address.
- [Low] Non-generic node name `syscon-reboot`.
--
--- Patch [12]: [PATCH v8 12/12] ARM: dts: zte: Declare zx297520v3 CRM device nodes ---
commit ae42492b8ef6fce4731646f6907ba63dc94e9ffa
Author: Stefan Dösinger <stefandoesinger@gmail.com>
ARM: dts: zte: Declare zx297520v3 CRM device nodes
This commit wires up the zx297520v3 CRM device nodes, clock inputs,
and resets, getting rid of the placeholder uartclk node.
> diff --git a/arch/arm/boot/dts/zte/zx297520v3.dtsi b/arch/arm/boot/dts/zte/zx297520v3.dtsi
> --- a/arch/arm/boot/dts/zte/zx297520v3.dtsi
> +++ b/arch/arm/boot/dts/zte/zx297520v3.dtsi
[ ... ]
> @@ -70,13 +75,74 @@ gic: interrupt-controller@f2000000 {
> <0xf2040000 0x20000>;
> };
>
> + topcrm: clock-controller@13b000 {
> + compatible = "zte,zx297520v3-topcrm", "syscon";
> + reg = <0x0013b000 0x400>;
[Severity: Low]
Are these new nodes inserted out of order relative to their unit addresses?
The topcrm node at 13b000 is placed before matrixcrm at 1306000, and lspcrm
at 1400000 is placed before uart0 at 131000. It is customary to sort nodes
under a bus by their unit address to maintain readability.
[ ... ]
> + #clock-cells = <1>;
> + #phy-cells = <1>;
> + #reset-cells = <1>;
> +
> + syscon-reboot {
> + compatible = "syscon-reboot";
> + offset = <0x0>;
> + value = <0x1>;
> + mask = <0x1>;
> + };
[Severity: Low]
Should this node use a generic name like reboot instead of syscon-reboot?
The Devicetree specification usually recommends node names to reflect the
function of the device rather than its programming model or driver name.
> + };
> +
> + matrixcrm: clock-controller@1306000 {
> + compatible = "zte,zx297520v3-matrixcrm";
> + reg = <0x01306000 0x400>;
[ ... ]
> + lspcrm: clock-controller@1400000 {
> + compatible = "zte,zx297520v3-lspcrm";
> + reg = <0x01400000 0x100>;
> + #clock-cells = <1>;
> + #reset-cells = <1>;
[ ... ]
> + clock-names = "mpll-d5", "mpll-d4", "mpll-d6", "mpll-d8", "mpll-d12",
> + "osc26m", "osc32k", "pclk", "tdm-wclk", "dpll-d4";
> + resets = <&matrixcrm ZX297520V3_LSP_RESET>;
> + };
[Severity: High]
Does this hardware reset dependency require explicit handling in the driver?
The DTS correctly defines the dependency, but looking at zx_clk_common_probe()
in drivers/clk/zte/clk-zx.c, the regmap is obtained using:
drivers/clk/zte/clk-zx.c:zx_clk_common_probe() {
...
map = device_node_to_regmap(of_node);
if (IS_ERR(map))
return PTR_ERR(map);
...
}
Because the lspcrm node lacks the syscon compatible string, this bypasses the
generic syscon reset handling. As neither the MFD driver nor child drivers
manually deassert this reset, won't subsequent register accesses hit a reset
block and potentially hang the hardware?
> +
> uart0: serial@131000 {
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260727-zx29clk-v8-0-7a107b00f1dd@gmail.com?part=12
^ permalink raw reply [flat|nested] 50+ messages in thread