All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH net-next v7 0/3] dpll: add SiTime SiT9531x DPLL clock driver
@ 2026-08-15 22:19 Ali Rouhi
  2026-08-15 22:19 ` [PATCH net-next v7 1/3] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
                   ` (2 more replies)
  0 siblings, 3 replies; 6+ messages in thread
From: Ali Rouhi @ 2026-08-15 22:19 UTC (permalink / raw)
  To: jiri@resnulli.us
  Cc: vadim.fedorenko@linux.dev, arkadiusz.kubalewski@intel.com,
	robh@kernel.org, krzk+dt@kernel.org, conor+dt@kernel.org,
	cjubran@nvidia.com, Oleg.Zadorozhnyi@devoxsoftware.com,
	devicetree@vger.kernel.org, netdev@vger.kernel.org,
	linux-kernel@vger.kernel.org, Ali Rouhi

This series adds a DPLL subsystem driver for the SiTime SiT95316 and
SiT95317 I2C clock generators. Each device integrates four PLLs with
automatic/manual reference selection and on-chip TDC phase-offset
measurement, and is used for synchronization in telecom, networking,
and data-center timing.

The series contains the device-tree binding, the driver under
drivers/dpll/sit9531x/, and the MAINTAINERS entry.

v1: https://lore.kernel.org/netdev/20260511211143.19792-1-arouhi@sitime.com/
v2: https://lore.kernel.org/netdev/20260520191943.73938-1-arouhi@sitime.com/
v3: https://lore.kernel.org/netdev/20260731180951.65725-1-arouhi@sitime.com/
v4: https://lore.kernel.org/netdev/20260806232439.27551-1-arouhi@sitime.com/
v5: https://lore.kernel.org/netdev/20260810230439.22866-1-arouhi@sitime.com/
v6: https://lore.kernel.org/netdev/20260812175337.18155-1-arouhi@sitime.com/

Changes since v6 (driver only; the bindings are unchanged):

  - Remove a conflict marker that slipped into a comment block in
    dpll.c. It sat inside /* */, so it built and checkpatch, which has
    no check for markers, did not report it.
  - Unwind DPLL registration when it fails partway. dev_start() returned
    on the first failure without unregistering the DPLLs already done,
    and the caller then freed them, leaving the subsystem with pointers
    into freed memory.
  - Report an input as selectable only for the DPLL that can select it.
    The test used the physical receiver state, which every DPLL fed from
    that lane shares. A lane in LOS no longer drops to disconnected
    either: signal quality belongs in the pin's own attributes.
  - Refuse DPLL_PIN_STATE_CONNECTED on an input instead of treating it
    as selectable. The device selects by priority and has no mode that
    pins one reference, so the request cannot be honoured.
  - Keep the rest of the priority table when one input moves. prio_set
    used to fill every lower-priority slot with the same source, which
    discarded the fallbacks the caller had not asked about.
  - Refuse to drive INTSYNC from a PLL that already selects it.
  - Drop the SYSREF/SYNCB/pulser claim from the commit message: only the
    esync default is reachable, the other modes are not exposed.

The AI review also reported a use-after-free of the pin properties. It
is a false positive: dpll_pin_alloc() calls dpll_pin_prop_dup(), which
copies freq_supported with kmemdup() and the labels with kstrdup(), so
the core owns its copies and the driver has to free its own.


Ali Rouhi (3):
  dt-bindings: vendor-prefixes: add SiTime Corporation
  dt-bindings: dpll: add SiTime SiT95316 clock generator
  dpll: add SiTime SiT9531x DPLL clock driver

 .../bindings/dpll/sitime,sit95316.yaml        |  171 +
 .../devicetree/bindings/vendor-prefixes.yaml  |    2 +
 MAINTAINERS                                   |    7 +
 drivers/dpll/Kconfig                          |    1 +
 drivers/dpll/Makefile                         |    1 +
 drivers/dpll/sit9531x/Kconfig                 |   17 +
 drivers/dpll/sit9531x/Makefile                |    4 +
 drivers/dpll/sit9531x/core.c                  | 3111 +++++++++++++++++
 drivers/dpll/sit9531x/core.h                  |  372 ++
 drivers/dpll/sit9531x/dpll.c                  | 1232 +++++++
 drivers/dpll/sit9531x/dpll.h                  |   69 +
 drivers/dpll/sit9531x/prop.c                  |  397 +++
 drivers/dpll/sit9531x/prop.h                  |   39 +
 drivers/dpll/sit9531x/regs.h                  |  371 ++
 14 files changed, 5794 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
 create mode 100644 drivers/dpll/sit9531x/Kconfig
 create mode 100644 drivers/dpll/sit9531x/Makefile
 create mode 100644 drivers/dpll/sit9531x/core.c
 create mode 100644 drivers/dpll/sit9531x/core.h
 create mode 100644 drivers/dpll/sit9531x/dpll.c
 create mode 100644 drivers/dpll/sit9531x/dpll.h
 create mode 100644 drivers/dpll/sit9531x/prop.c
 create mode 100644 drivers/dpll/sit9531x/prop.h
 create mode 100644 drivers/dpll/sit9531x/regs.h


base-commit: 001b5d347d8ba39b2dccaefcc57967b18caec8fe
-- 
2.34.1


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH net-next v7 1/3] dt-bindings: vendor-prefixes: add SiTime Corporation
  2026-08-15 22:19 [PATCH net-next v7 0/3] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
@ 2026-08-15 22:19 ` Ali Rouhi
  2026-08-15 22:19 ` [PATCH net-next v7 2/3] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
  2026-08-15 22:19 ` [PATCH net-next v7 3/3] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
  2 siblings, 0 replies; 6+ messages in thread
From: Ali Rouhi @ 2026-08-15 22:19 UTC (permalink / raw)
  To: jiri@resnulli.us
  Cc: vadim.fedorenko@linux.dev, arkadiusz.kubalewski@intel.com,
	robh@kernel.org, krzk+dt@kernel.org, conor+dt@kernel.org,
	cjubran@nvidia.com, Oleg.Zadorozhnyi@devoxsoftware.com,
	devicetree@vger.kernel.org, netdev@vger.kernel.org,
	linux-kernel@vger.kernel.org, Ali Rouhi, Conor Dooley

Add vendor prefix for SiTime Corporation, manufacturer of
programmable clock generators and MEMS oscillators.

Signed-off-by: Ali Rouhi <arouhi@sitime.com>
Acked-by: Conor Dooley <conor.dooley@microchip.com>
---
 Documentation/devicetree/bindings/vendor-prefixes.yaml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml
index 396044f368e7..1921718a5f82 100644
--- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
+++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
@@ -1535,6 +1535,8 @@ patternProperties:
     description: SiRF Technology, Inc.
   "^sis,.*":
     description: Silicon Integrated Systems Corp.
+  "^sitime,.*":
+    description: SiTime Corporation
   "^sitronix,.*":
     description: Sitronix Technology Corporation
   "^skov,.*":
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH net-next v7 2/3] dt-bindings: dpll: add SiTime SiT95316 clock generator
  2026-08-15 22:19 [PATCH net-next v7 0/3] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
  2026-08-15 22:19 ` [PATCH net-next v7 1/3] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
@ 2026-08-15 22:19 ` Ali Rouhi
  2026-08-16 22:19   ` sashiko-bot
  2026-08-15 22:19 ` [PATCH net-next v7 3/3] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
  2 siblings, 1 reply; 6+ messages in thread
From: Ali Rouhi @ 2026-08-15 22:19 UTC (permalink / raw)
  To: jiri@resnulli.us
  Cc: vadim.fedorenko@linux.dev, arkadiusz.kubalewski@intel.com,
	robh@kernel.org, krzk+dt@kernel.org, conor+dt@kernel.org,
	cjubran@nvidia.com, Oleg.Zadorozhnyi@devoxsoftware.com,
	devicetree@vger.kernel.org, netdev@vger.kernel.org,
	linux-kernel@vger.kernel.org, Ali Rouhi, Krzysztof Kozlowski

Add device tree binding documentation for the SiTime SiT95316
and SiT95317 DPLL clock generators.

Signed-off-by: Ali Rouhi <arouhi@sitime.com>
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>
---
 .../bindings/dpll/sitime,sit95316.yaml        | 171 ++++++++++++++++++
 1 file changed, 171 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml

diff --git a/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml b/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
new file mode 100644
index 000000000000..591a8c0d7bbf
--- /dev/null
+++ b/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
@@ -0,0 +1,171 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/dpll/sitime,sit95316.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: SiTime SiT95316/SiT95317 DPLL Clock Generator
+
+maintainers:
+  - Ali Rouhi <arouhi@sitime.com>
+
+description: |
+  SiTime SiT95316 and SiT95317 are I2C-controlled programmable clock
+  generators with integrated DPLL for synchronization applications.  Both
+  variants contain four PLLs with automatic/manual reference selection,
+  DCO frequency adjustment, and phase offset measurement via an on-chip
+  TDC (Time-to-Digital Converter).
+
+  Both parts have 4 differential input pairs whose lanes can also be
+  driven independently as single-ended references, so 8 inputs are
+  individually selectable.  SiT95317 drives 8 outputs, SiT95316
+  drives 12.
+
+properties:
+  compatible:
+    enum:
+      - sitime,sit95316
+      - sitime,sit95317
+
+  reg:
+    maxItems: 1
+
+  clocks:
+    maxItems: 1
+
+  clock-names:
+    items:
+      - const: xtal
+
+  reset-gpios:
+    maxItems: 1
+    description:
+      GPIO connected to the chip's active-low reset pin (RESETB).
+
+  interrupts:
+    maxItems: 1
+    description:
+      Interrupt from the chip's active-low INTRB output.  Asserted when
+      the device detects a status change such as lock acquisition or loss.
+
+  sitime,pll-fvco:
+    $ref: /schemas/types.yaml#/definitions/uint64-array
+    minItems: 4
+    maxItems: 4
+    description:
+      Per-PLL VCO frequency in Hz for PLLA, PLLB, PLLC, PLLD.  Neither
+      standard frequency suffix fits.  The VCO bands are 4915.2 MHz to
+      5898.24 MHz and 6875 MHz to 7812.5 MHz, so every valid value
+      exceeds the 32 bits "-hz" allows, and the band edges are not
+      whole megahertz either, so "-mhz" cannot express them.  Hence a
+      plain uint64-array.  Override where the standard
+      Fvco = Fref * DIVN derivation does not match the running VCO --
+      for example a PLL operating in INTSYNC mode.  A value of 0 keeps
+      the register-derived computation for that PLL.
+
+  sitime,output-pll-map:
+    $ref: /schemas/types.yaml#/definitions/uint32-array
+    minItems: 8
+    maxItems: 12
+    items:
+      enum: [0, 1, 2, 3, 255]
+    description:
+      Source PLL index (0=PLLA .. 3=PLLD) for each output 0..11.  The
+      value 255 (0xff) marks an output as unmapped and prevents the
+      driver from registering it as a DPLL pin.  Override for
+      configurations where the chip's per-PLL OUTPUT_ENABLE bitmaps do
+      not unambiguously describe output-to-PLL routing.
+
+required:
+  - compatible
+  - reg
+  - clocks
+  - clock-names
+
+allOf:
+  - $ref: /schemas/dpll/dpll-device.yaml#
+  # SiT95317 exposes 8 outputs, SiT95316 exposes 12.  Bound the
+  # output-pll-map length to the variant so a SiT95317 node cannot
+  # describe more outputs than the part has.
+  - if:
+      properties:
+        compatible:
+          contains:
+            const: sitime,sit95317
+    then:
+      properties:
+        sitime,output-pll-map:
+          maxItems: 8
+  # SiT95316 has 12 outputs.  When the map is supplied it must describe
+  # all of them, otherwise the trailing outputs are left ambiguous.
+  - if:
+      properties:
+        compatible:
+          contains:
+            const: sitime,sit95316
+    then:
+      properties:
+        sitime,output-pll-map:
+          minItems: 12
+
+unevaluatedProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/gpio/gpio.h>
+    #include <dt-bindings/interrupt-controller/irq.h>
+
+    i2c {
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        dpll@68 {
+            compatible = "sitime,sit95316";
+            reg = <0x68>;
+            clocks = <&xo2>;
+            clock-names = "xtal";
+            reset-gpios = <&gpio 78 GPIO_ACTIVE_LOW>;
+            interrupts = <12 IRQ_TYPE_LEVEL_LOW>;
+            dpll-types = "eec", "eec", "eec", "eec";
+            sitime,pll-fvco = /bits/ 64 <6251500000 0 0 0>;
+            sitime,output-pll-map = <0 0 0 0 0 0 0 0 0 0 0 0>;
+
+            input-pins {
+                #address-cells = <1>;
+                #size-cells = <0>;
+
+                pin@0 {
+                    reg = <0>;
+                    label = "clkin0";
+                    connection-type = "ext";
+                    supported-frequencies-hz = /bits/ 64 <10000000>;
+                };
+
+                pin@1 {
+                    reg = <1>;
+                    label = "clkin1";
+                    connection-type = "synce";
+                    supported-frequencies-hz = /bits/ 64 <156250000>;
+                };
+            };
+
+            output-pins {
+                #address-cells = <1>;
+                #size-cells = <0>;
+
+                pin@0 {
+                    reg = <0>;
+                    label = "clkout0";
+                    esync-control;
+                    supported-frequencies-hz = /bits/ 64 <156250000>;
+                };
+
+                pin@1 {
+                    reg = <1>;
+                    label = "clkout1";
+                    supported-frequencies-hz = /bits/ 64 <25000000>;
+                };
+            };
+        };
+    };
+...
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH net-next v7 3/3] dpll: add SiTime SiT9531x DPLL clock driver
  2026-08-15 22:19 [PATCH net-next v7 0/3] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
  2026-08-15 22:19 ` [PATCH net-next v7 1/3] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
  2026-08-15 22:19 ` [PATCH net-next v7 2/3] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
@ 2026-08-15 22:19 ` Ali Rouhi
  2026-08-16 22:19   ` sashiko-bot
  2 siblings, 1 reply; 6+ messages in thread
From: Ali Rouhi @ 2026-08-15 22:19 UTC (permalink / raw)
  To: jiri@resnulli.us
  Cc: vadim.fedorenko@linux.dev, arkadiusz.kubalewski@intel.com,
	robh@kernel.org, krzk+dt@kernel.org, conor+dt@kernel.org,
	cjubran@nvidia.com, Oleg.Zadorozhnyi@devoxsoftware.com,
	devicetree@vger.kernel.org, netdev@vger.kernel.org,
	linux-kernel@vger.kernel.org, Ali Rouhi

Add a DPLL subsystem driver for the SiTime SiT95316 and SiT95317
clock generators. These devices provide low-jitter clock outputs
commonly used in telecom, networking, and data center timing
applications.

The driver exposes all inputs and outputs through the Linux DPLL
subsystem, supporting:
 - Lock status monitoring via register polling or optional INTRB IRQ
 - Input priority management for automatic reference switchover
 - Per-output frequency readback from hardware state
 - Phase offset measurement via TDC (time-to-digital converter)
 - Phase adjustment for fine output alignment
 - Embedded sync (esync) pulse control on outputs
 - Fractional frequency offset of the selected reference
 - Optional reset-gpios for hardware reset

The driver reads all configuration from the device's on-chip NVM
at probe time -- no firmware loading is required.

Co-developed-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 MAINTAINERS                    |    7 +
 drivers/dpll/Kconfig           |    1 +
 drivers/dpll/Makefile          |    1 +
 drivers/dpll/sit9531x/Kconfig  |   17 +
 drivers/dpll/sit9531x/Makefile |    4 +
 drivers/dpll/sit9531x/core.c   | 3111 ++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/core.h   |  372 ++++
 drivers/dpll/sit9531x/dpll.c   | 1232 +++++++++++++
 drivers/dpll/sit9531x/dpll.h   |   69 +
 drivers/dpll/sit9531x/prop.c   |  397 ++++
 drivers/dpll/sit9531x/prop.h   |   39 +
 drivers/dpll/sit9531x/regs.h   |  371 ++++
 12 files changed, 5621 insertions(+)
 create mode 100644 drivers/dpll/sit9531x/Kconfig
 create mode 100644 drivers/dpll/sit9531x/Makefile
 create mode 100644 drivers/dpll/sit9531x/core.c
 create mode 100644 drivers/dpll/sit9531x/core.h
 create mode 100644 drivers/dpll/sit9531x/dpll.c
 create mode 100644 drivers/dpll/sit9531x/dpll.h
 create mode 100644 drivers/dpll/sit9531x/prop.c
 create mode 100644 drivers/dpll/sit9531x/prop.h
 create mode 100644 drivers/dpll/sit9531x/regs.h

diff --git a/MAINTAINERS b/MAINTAINERS
index a82b6ec9c567..8dfab3ac6a17 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -24905,6 +24905,13 @@ S:	Maintained
 W:	http://www.winischhofer.at/linuxsisusbvga.shtml
 F:	drivers/usb/misc/sisusbvga/
 
+SITIME SIT9531X DPLL DRIVER
+M:	Ali Rouhi <arouhi@sitime.com>
+L:	netdev@vger.kernel.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
+F:	drivers/dpll/sit9531x/
+
 SL28 CPLD MFD DRIVER
 M:	Michael Walle <mwalle@kernel.org>
 S:	Maintained
diff --git a/drivers/dpll/Kconfig b/drivers/dpll/Kconfig
index be98969f040a..ea77b9c11ab1 100644
--- a/drivers/dpll/Kconfig
+++ b/drivers/dpll/Kconfig
@@ -23,6 +23,7 @@ config DPLL_REFCNT_TRACKER
 
 	  If unsure, say N.
 
+source "drivers/dpll/sit9531x/Kconfig"
 source "drivers/dpll/zl3073x/Kconfig"
 
 endmenu
diff --git a/drivers/dpll/Makefile b/drivers/dpll/Makefile
index 9e7a3a3e592e..4adc50d748d4 100644
--- a/drivers/dpll/Makefile
+++ b/drivers/dpll/Makefile
@@ -8,4 +8,5 @@ dpll-y                  += dpll_core.o
 dpll-y                  += dpll_netlink.o
 dpll-y                  += dpll_nl.o
 
+obj-$(CONFIG_SIT9531X_DPLL)	+= sit9531x/
 obj-$(CONFIG_ZL3073X)	+= zl3073x/
diff --git a/drivers/dpll/sit9531x/Kconfig b/drivers/dpll/sit9531x/Kconfig
new file mode 100644
index 000000000000..47aea8674327
--- /dev/null
+++ b/drivers/dpll/sit9531x/Kconfig
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+config SIT9531X_DPLL
+	tristate "SiTime SiT9531x DPLL driver"
+	depends on I2C && NET
+	select DPLL
+	select REGMAP_I2C
+	help
+	  Driver for SiTime SiT9531x family clock generators
+	  (SiT95317, SiT95316).
+
+	  This driver registers each on-chip PLL as a DPLL device
+	  and exposes input/output clocks as DPLL pins, providing
+	  runtime configuration via Generic Netlink.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called sit9531x.
diff --git a/drivers/dpll/sit9531x/Makefile b/drivers/dpll/sit9531x/Makefile
new file mode 100644
index 000000000000..b97d2656a460
--- /dev/null
+++ b/drivers/dpll/sit9531x/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+obj-$(CONFIG_SIT9531X_DPLL) += sit9531x.o
+sit9531x-y := core.o dpll.o prop.o
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
new file mode 100644
index 000000000000..cd283922e7ff
--- /dev/null
+++ b/drivers/dpll/sit9531x/core.c
@@ -0,0 +1,3111 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * SiTime SiT9531x DPLL core driver
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * Core I2C probe, regmap configuration, hardware state management,
+ * and periodic work thread.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/device.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/kernel.h>
+#include <linux/kthread.h>
+#include <linux/module.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#include "core.h"
+#include "dpll.h"
+#include "prop.h"
+#include "regs.h"
+
+/* Number of input + output pin positions for pin index allocation */
+#define SIT9531X_NUM_INPUT_PINS		(SIT9531X_MAX_INPUTS + 2) /* +xtal +INTSYNC */
+#define SIT9531X_NUM_OUTPUT_PINS	(SIT9531X_MAX_OUTPUTS + 1) /* +INTSYNC src */
+#define SIT9531X_NUM_PINS_TOTAL		(SIT9531X_NUM_INPUT_PINS + SIT9531X_NUM_OUTPUT_PINS)
+
+#define SIT9531X_CHIP(_id, _nin, _nout, _name, _map) \
+	{ .id = (_id), .num_inputs = (_nin), .num_outputs = (_nout), \
+	  .name = (_name), .clkout_map = (_map) }
+
+/* Per-variant output index -> physical slot mapping */
+static const u8 clkout_map_95317[] = {0, 3, 4, 5, 7, 8, 9, 11};
+static const u8 clkout_map_95316[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
+
+static const struct sit9531x_chip_info sit9531x_chip_ids[] = {
+	SIT9531X_CHIP(SIT9531X_VARIANT_ID_95317,  8,  8, "SiT95317", clkout_map_95317),
+	SIT9531X_CHIP(SIT9531X_VARIANT_ID_95316,  8, 12, "SiT95316", clkout_map_95316),
+};
+
+#define SIT9531X_RANGE_OFFSET	SIT9531X_PAGE_SIZE
+
+static const struct regmap_range_cfg sit9531x_regmap_range = {
+	.range_min	= SIT9531X_RANGE_OFFSET,
+	.range_max	= SIT9531X_RANGE_OFFSET +
+			  (SIT9531X_NUM_PAGES * SIT9531X_PAGE_SIZE) - 1,
+	.selector_reg	= SIT9531X_PAGE_SEL,
+	.selector_mask	= GENMASK(7, 0),
+	.selector_shift	= 0,
+	.window_start	= 0,
+	.window_len	= SIT9531X_PAGE_SIZE,
+};
+
+const struct regmap_config sit9531x_regmap_config = {
+	.reg_bits	= 8,
+	.val_bits	= 8,
+	.max_register	= SIT9531X_RANGE_OFFSET +
+			  (SIT9531X_NUM_PAGES * SIT9531X_PAGE_SIZE) - 1,
+	.ranges		= &sit9531x_regmap_range,
+	.num_ranges	= 1,
+	.cache_type	= REGCACHE_NONE,
+};
+
+/*
+ * sit9531x_read_u8 - read an 8-bit register
+ * @reg:	register in SIT9531X_REG(page, offset) form
+ * @val:	output value
+ */
+int sit9531x_read_u8(struct sit9531x_dev *sitdev, unsigned int reg,
+		     u8 *val)
+{
+	unsigned int tmp;
+	int rc;
+
+	reg = (SIT9531X_REG_PAGE(reg) * SIT9531X_PAGE_SIZE) +
+	      SIT9531X_REG_OFFSET(reg) + SIT9531X_RANGE_OFFSET;
+
+	rc = regmap_read(sitdev->regmap, reg, &tmp);
+	if (rc)
+		dev_err(sitdev->dev, "Failed to read reg 0x%04x: %d\n",
+			reg, rc);
+	else
+		*val = (u8)tmp;
+
+	return rc;
+}
+
+/*
+ * sit9531x_write_u8 - write an 8-bit register
+ * @reg:	register in SIT9531X_REG(page, offset) form
+ * @val:	value to write
+ */
+int sit9531x_write_u8(struct sit9531x_dev *sitdev, unsigned int reg,
+		      u8 val)
+{
+	int rc;
+
+	reg = (SIT9531X_REG_PAGE(reg) * SIT9531X_PAGE_SIZE) +
+	      SIT9531X_REG_OFFSET(reg) + SIT9531X_RANGE_OFFSET;
+
+	rc = regmap_write(sitdev->regmap, reg, val);
+	if (rc)
+		dev_err(sitdev->dev, "Failed to write reg 0x%04x: %d\n",
+			reg, rc);
+
+	return rc;
+}
+
+/*
+ * sit9531x_read_pll_u8 - read a register on a PLL page
+ * @val:	output value
+ */
+int sit9531x_read_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+			 u8 offset, u8 *val)
+{
+	return sit9531x_read_u8(sitdev,
+				SIT9531X_REG(sit9531x_pll_page(pll_idx), offset),
+				val);
+}
+
+/*
+ * sit9531x_write_pll_u8 - write a register on a PLL page
+ * @val:	value to write
+ */
+int sit9531x_write_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+			  u8 offset, u8 val)
+{
+	return sit9531x_write_u8(sitdev,
+				 SIT9531X_REG(sit9531x_pll_page(pll_idx), offset),
+				 val);
+}
+
+/*
+ * sit9531x_update_pll_u8 - read-modify-write a register on a PLL page
+ * @mask:	bits to modify
+ * @val:	new value for masked bits
+ */
+int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+			   u8 offset, u8 mask, u8 val)
+{
+	unsigned int reg;
+
+	reg = (sit9531x_pll_page(pll_idx) * SIT9531X_PAGE_SIZE) +
+	      offset + SIT9531X_RANGE_OFFSET;
+
+	return regmap_update_bits(sitdev->regmap, reg, mask, val);
+}
+
+/*
+ * sit9531x_input_get_regs - get force mask and state register addresses
+ * @index:	logical input index
+ * @force_reg:	output force mask register address
+ * @state_reg:	output state register address
+ *
+ * Selects the correct Page 0x02 register pair based on the pair's
+ * signal mode and the lane (P/N) the index refers to.
+ */
+static void sit9531x_input_get_regs(const struct sit9531x_dev *sitdev,
+				    u8 index,
+				    unsigned int *force_reg,
+				    unsigned int *state_reg)
+{
+	if (sitdev->ref[index].sig_mode == SIT9531X_MODE_DE) {
+		*force_reg = SIT9531X_REG_IN_DE_FORCE;
+		*state_reg = SIT9531X_REG_IN_DE_STATE;
+	} else if (sit9531x_input_is_n(index)) {
+		*force_reg = SIT9531X_REG_IN_SEN_FORCE;
+		*state_reg = SIT9531X_REG_IN_SEN_STATE;
+	} else {
+		*force_reg = SIT9531X_REG_IN_SEP_FORCE;
+		*state_reg = SIT9531X_REG_IN_SEP_STATE;
+	}
+}
+
+/*
+ * sit9531x_input_disable - disable an input reference
+ * @index:	logical input index (0-N)
+ *
+ * Sets the force mask bit and clears the state bit for the given
+ * input, effectively disabling it.  Register selection depends on
+ * the pair's signal mode (SE/DE) and the lane (P/N); the bit within
+ * each register addresses the input pair.
+ */
+int sit9531x_input_disable(struct sit9531x_dev *sitdev, u8 index)
+{
+	struct sit9531x_ref *ref = &sitdev->ref[index];
+	unsigned int force_reg, state_reg;
+	u8 pair = sit9531x_input_pair(index);
+	u8 val;
+	int rc;
+
+	sit9531x_input_get_regs(sitdev, index, &force_reg, &state_reg);
+
+	rc = sit9531x_read_u8(sitdev, force_reg, &val);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, force_reg, val | BIT(pair));
+	if (rc)
+		return rc;
+
+	rc = sit9531x_read_u8(sitdev, state_reg, &val);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, state_reg, val & ~BIT(pair));
+	if (rc)
+		return rc;
+
+	ref->enabled = false;
+
+	return 0;
+}
+
+/*
+ * sit9531x_input_enable - enable an input reference
+ * @index:	logical input index (0-N)
+ *
+ * Clears the force mask bit for the given input, returning it to
+ * hardware default (enabled).
+ */
+int sit9531x_input_enable(struct sit9531x_dev *sitdev, u8 index)
+{
+	struct sit9531x_ref *ref = &sitdev->ref[index];
+	unsigned int force_reg, state_reg;
+	u8 pair = sit9531x_input_pair(index);
+	u8 val;
+	int rc;
+
+	sit9531x_input_get_regs(sitdev, index, &force_reg, &state_reg);
+
+	rc = sit9531x_read_u8(sitdev, force_reg, &val);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, force_reg, val & ~BIT(pair));
+	if (rc)
+		return rc;
+
+	ref->enabled = true;
+
+	return 0;
+}
+
+/*
+ * Output enable / disable (Hi-Z control)
+ *
+ * SiT9531x outputs can be configured as differential (DIFF) or
+ * single-ended (SE) depending on the factory blob.  Each output slot
+ * has TWO Hi-Z force/state register pairs on Page 0x03 -- one for the
+ * DIFF path, one for the SE path.
+ *
+ * We write to BOTH pairs so the function mutes the output regardless
+ * of whether it's been configured DIFF or SE on this board.
+ *
+ *   slot 0-7 :
+ *     DIFF mask=0xF2 state=0xF3   SE mask=0xF8 state=0xF9
+ *   slot 8-11:
+ *     DIFF mask=0xF4 state=0xF5   SE mask=0xFA state=0xFB
+ *
+ * MASK bit = 1  -> driver takes control of that output's Hi-Z state
+ * STATE bit = 0 -> output is forced to Hi-Z (muted)
+ * STATE bit = 1 -> output is driven (active)
+ *
+ * The output "index" in the driver is logical; the physical slot comes
+ * from info->clkout_map[].
+ */
+
+struct sit9531x_hiz_regs {
+	unsigned int diff_mask;
+	unsigned int diff_state;
+	unsigned int se_mask;
+	unsigned int se_state;
+	u8 bit;
+};
+
+static void sit9531x_output_get_hiz_regs(u8 slot,
+					 struct sit9531x_hiz_regs *r)
+{
+	if (slot <= 7) {
+		r->diff_mask  = SIT9531X_REG_HIZ_DIFF_07_MASK;
+		r->diff_state = SIT9531X_REG_HIZ_DIFF_07_STATE;
+		r->se_mask    = SIT9531X_REG_HIZ_SE_07_MASK;
+		r->se_state   = SIT9531X_REG_HIZ_SE_07_STATE;
+		r->bit = slot;
+	} else {
+		r->diff_mask  = SIT9531X_REG_HIZ_DIFF_811_MASK;
+		r->diff_state = SIT9531X_REG_HIZ_DIFF_811_STATE;
+		r->se_mask    = SIT9531X_REG_HIZ_SE_811_MASK;
+		r->se_state   = SIT9531X_REG_HIZ_SE_811_STATE;
+		r->bit = slot - 8;
+	}
+}
+
+/*
+ * Report whether a slot is currently forced into Hi-Z, i.e. the driver
+ * (or the blob) took control of the Hi-Z state (MASK bit set) and drives
+ * it low (STATE bit clear).  Either register pair muting the slot counts,
+ * mirroring what sit9531x_output_disable() programs.
+ */
+static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev, u8 slot,
+				      bool *muted)
+{
+	struct sit9531x_hiz_regs r;
+	u8 mask, state;
+	int rc;
+
+	sit9531x_output_get_hiz_regs(slot, &r);
+
+	rc = sit9531x_read_u8(sitdev, r.diff_mask, &mask);
+	if (rc)
+		return rc;
+	rc = sit9531x_read_u8(sitdev, r.diff_state, &state);
+	if (rc)
+		return rc;
+
+	*muted = (mask & BIT(r.bit)) && !(state & BIT(r.bit));
+	if (*muted)
+		return 0;
+
+	rc = sit9531x_read_u8(sitdev, r.se_mask, &mask);
+	if (rc)
+		return rc;
+	rc = sit9531x_read_u8(sitdev, r.se_state, &state);
+	if (rc)
+		return rc;
+
+	*muted = (mask & BIT(r.bit)) && !(state & BIT(r.bit));
+
+	return 0;
+}
+
+static int sit9531x_hiz_set_bit(struct sit9531x_dev *sitdev,
+				unsigned int reg, u8 bit, bool set)
+{
+	u8 cur, new_val;
+	int rc;
+
+	rc = sit9531x_read_u8(sitdev, reg, &cur);
+	if (rc)
+		return rc;
+
+	new_val = set ? (cur | BIT(bit)) : (cur & ~BIT(bit));
+
+	return sit9531x_write_u8(sitdev, reg, new_val);
+}
+
+/*
+ * Enter the output-system programming state: unlock the debug
+ * registers on Page 3 and issue the PRG_CMD state command.  Register
+ * writes that reconfigure the output system only take effect when
+ * they are made inside this state.
+ */
+static int sit9531x_prg_enter(struct sit9531x_dev *sitdev)
+{
+	int rc;
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
+			       SIT9531X_DEBUG_UNLOCK_VAL);
+	if (rc)
+		return rc;
+
+	return sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+				 SIT9531X_PRG_CMD_STATE);
+}
+
+/*
+ * Commit a programming sequence started by sit9531x_prg_enter():
+ * update the NVM shadow and re-lock the loops.  The sleep gives the
+ * hardware its required settling time after the loop-lock command;
+ * it is intentional despite the caller holding multiop_lock, as the
+ * whole NVM + lock sequence must be atomic.
+ */
+static int sit9531x_prg_commit(struct sit9531x_dev *sitdev)
+{
+	int rc;
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+			       SIT9531X_UPDATE_NVM);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+			       SIT9531X_LOOP_LOCK);
+	if (rc)
+		return rc;
+
+	msleep(100);
+
+	return 0;
+}
+
+/*
+ * sit9531x_output_disable - mute an output (force Hi-Z)
+ * @index:	logical output index (0..info->num_outputs-1)
+ *
+ * Sets MASK+STATE on BOTH the DIFF and SE register pairs so that the
+ * output is muted regardless of its electrical configuration.  The
+ * writes are wrapped in the PRG_CMD / NVM update / loop lock sequence
+ * so the new state is applied by the hardware.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_output_disable(struct sit9531x_dev *sitdev, u8 index)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	struct sit9531x_hiz_regs r;
+	u8 slot;
+	int rc, ret;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (index >= info->num_outputs)
+		return -EINVAL;
+
+	slot = info->clkout_map[index];
+	sit9531x_output_get_hiz_regs(slot, &r);
+
+	rc = sit9531x_prg_enter(sitdev);
+	if (rc)
+		return rc;
+
+	/* Take control (MASK=1) and mute (STATE=0) on both DIFF and SE */
+	rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, true);
+	if (rc)
+		goto commit;
+	rc = sit9531x_hiz_set_bit(sitdev, r.diff_state, r.bit, false);
+	if (rc)
+		goto commit;
+	rc = sit9531x_hiz_set_bit(sitdev, r.se_mask, r.bit, true);
+	if (rc)
+		goto commit;
+	rc = sit9531x_hiz_set_bit(sitdev, r.se_state, r.bit, false);
+
+commit:
+	/*
+	 * Always leave the PRG_CMD programming state, even on a mid-sequence
+	 * write failure: prg_enter() unlocked the output loops, so returning
+	 * without prg_commit() would strand the chip in the programming state
+	 * with the loops unlocked.  Best effort -- keep the first error.
+	 */
+	ret = sit9531x_prg_commit(sitdev);
+	if (ret && !rc)
+		rc = ret;
+	if (!rc)
+		sitdev->out[index].enabled = false;
+
+	return rc;
+}
+
+/*
+ * sit9531x_output_enable - un-mute an output (active state)
+ * @index:	logical output index (0..info->num_outputs-1)
+ *
+ * Releases MASK on BOTH register pairs so the output returns to
+ * whatever the initial_config blob programmed.  The writes are wrapped
+ * in the PRG_CMD / NVM update / loop lock sequence so the new state is
+ * applied by the hardware.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_output_enable(struct sit9531x_dev *sitdev, u8 index)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	struct sit9531x_hiz_regs r;
+	u8 slot;
+	int rc, ret;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (index >= info->num_outputs)
+		return -EINVAL;
+
+	slot = info->clkout_map[index];
+	sit9531x_output_get_hiz_regs(slot, &r);
+
+	rc = sit9531x_prg_enter(sitdev);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, false);
+	if (rc)
+		goto commit;
+	rc = sit9531x_hiz_set_bit(sitdev, r.se_mask, r.bit, false);
+
+commit:
+	/*
+	 * Always leave the PRG_CMD programming state, even on a mid-sequence
+	 * write failure: prg_enter() unlocked the output loops, so returning
+	 * without prg_commit() would strand the chip in the programming state
+	 * with the loops unlocked.  Best effort -- keep the first error.
+	 */
+	ret = sit9531x_prg_commit(sitdev);
+	if (ret && !rc)
+		rc = ret;
+	if (!rc)
+		sitdev->out[index].enabled = true;
+
+	return rc;
+}
+
+/*
+ * Input priority selection
+ *
+ * The SiT9531x has a 12-slot priority table per PLL on Page 1.  Each
+ * register holds two slots nibble-packed (even slot in [3:0], odd slot
+ * in [7:4]).
+ *
+ * The procedure:
+ *   1. Force PLL into holdover (PLL page reg 0x6F bit 4)
+ *   2. Write priority slots on Page 1
+ *   3. Small change update (Page 0 reg 0x0F bit 1)
+ *   4. Release holdover
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+
+/* Page-1 register holding priority slot @slot of @pll_idx. */
+static u16 sit9531x_prio_reg(u8 pll_idx, u8 slot)
+{
+	return SIT9531X_REG(SIT9531X_PAGE_PRIOSYS,
+			    SIT9531X_PRIO_BASE_REG +
+			    SIT9531X_PRIO_REGS_PER_PLL * pll_idx +
+			    slot / SIT9531X_PRIO_SLOTS_PER_REG);
+}
+
+/*
+ * Extract priority slot @slot from its register value.  The register
+ * holding slots 2n and 2n+1 keeps the earlier slot in the high nibble
+ * (CLK_SPARE<2n>SEL) and the later one in the low nibble.
+ */
+static u8 sit9531x_prio_slot_get(u8 val, u8 slot)
+{
+	if (slot & 1)
+		return val & SIT9531X_PRIO_NIBBLE_MASK;
+
+	return val >> SIT9531X_PRIO_HI_SHIFT;
+}
+
+/* Place source @src in priority slot @slot of a register value. */
+static u8 sit9531x_prio_slot_set(u8 val, u8 slot, u8 src)
+{
+	if (slot & 1)
+		return (val & (SIT9531X_PRIO_NIBBLE_MASK <<
+			       SIT9531X_PRIO_HI_SHIFT)) |
+		       (src & SIT9531X_PRIO_NIBBLE_MASK);
+
+	return (val & SIT9531X_PRIO_NIBBLE_MASK) |
+	       ((src & SIT9531X_PRIO_NIBBLE_MASK) <<
+		SIT9531X_PRIO_HI_SHIFT);
+}
+
+/*
+ * Commit a priority-table programming sequence through the Page-0
+ * programming directive register.
+ *
+ * A small change update is all the table needs.  The NVM-bank and
+ * loop-lock directives that the output system issues do not belong
+ * here: the former programs non-volatile storage from the efuse and
+ * the latter only means anything after an escape to the PRG_CMD
+ * state.  This matches the vendor input_priority_sel() procedure.
+ */
+static int sit9531x_prio_prg_commit(struct sit9531x_dev *sitdev)
+{
+	int rc;
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+			       SIT9531X_SMALL_UPDATE_CMD);
+	if (rc)
+		return rc;
+
+	usleep_range(1000, 2000);
+
+	return 0;
+}
+
+/*
+ * sit9531x_input_prio_get - read an input's priority slot for a PLL
+ * @input_idx:	input source in hardware encoding (see
+ *		sit9531x_input_hw_src())
+ * @prio:	output slot position (0 = highest); set to
+ *		SIT9531X_PRIO_MAX_SLOTS when the source is not in the table
+ *
+ * Scans the PLL's 12-slot priority table on Page 1 and returns the
+ * highest-priority (lowest-numbered) slot that references the source.
+ * This reads the value the chip actually holds rather than a cached
+ * default, so pin-get reflects the real hardware priority.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_input_prio_get(struct sit9531x_dev *sitdev, u8 pll_idx,
+			    u8 input_idx, u8 *prio)
+{
+	u8 val, slot, src;
+	int rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+		rc = sit9531x_read_u8(sitdev,
+				      sit9531x_prio_reg(pll_idx, slot), &val);
+		if (rc)
+			return rc;
+
+		src = sit9531x_prio_slot_get(val, slot);
+
+		if (src == input_idx) {
+			*prio = slot;
+			return 0;
+		}
+	}
+
+	*prio = SIT9531X_PRIO_MAX_SLOTS;
+	return 0;
+}
+
+/*
+ * sit9531x_prio_table_commit - write a full priority table for a PLL
+ * @srcs:	array of SIT9531X_PRIO_MAX_SLOTS source codes, slot 0 first
+ *
+ * Programs all priority slots (nibble-packed, two per register) for
+ * the PLL using the same holdover / small-update sequence as
+ * sit9531x_input_prio_set().  Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_prio_table_commit(struct sit9531x_dev *sitdev, u8 pll_idx,
+				      const u8 *srcs)
+{
+	u8 val, slot;
+	int rc, prg_rc, ho_rc;
+	u16 reg;
+
+	rc = sit9531x_update_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_HO_CTRL,
+				    BIT(SIT9531X_PLL_HO_FORCE_BIT),
+				    BIT(SIT9531X_PLL_HO_FORCE_BIT));
+	if (rc)
+		return rc;
+
+	usleep_range(10000, 12000);
+
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+		reg = sit9531x_prio_reg(pll_idx, slot);
+
+		rc = sit9531x_read_u8(sitdev, reg, &val);
+		if (rc)
+			goto commit;
+
+		val = sit9531x_prio_slot_set(val, slot, srcs[slot]);
+
+		rc = sit9531x_write_u8(sitdev, reg, val);
+		if (rc)
+			goto commit;
+	}
+
+commit:
+	/* Latch unconditionally, as in sit9531x_input_prio_set(). */
+	prg_rc = sit9531x_prio_prg_commit(sitdev);
+	if (prg_rc && !rc)
+		rc = prg_rc;
+
+	ho_rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+				       SIT9531X_PLL_REG_HO_CTRL,
+				       BIT(SIT9531X_PLL_HO_FORCE_BIT), 0);
+	if (ho_rc && !rc)
+		rc = ho_rc;
+
+	return rc;
+}
+
+/*
+ * sit9531x_prio_table_read - read a PLL's priority-table source codes
+ * @srcs:	output array of SIT9531X_PRIO_MAX_SLOTS source codes
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_prio_table_read(struct sit9531x_dev *sitdev, u8 pll_idx,
+				    u8 *srcs)
+{
+	u8 val, slot;
+	int rc;
+
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+		rc = sit9531x_read_u8(sitdev,
+				      sit9531x_prio_reg(pll_idx, slot), &val);
+		if (rc)
+			return rc;
+
+		srcs[slot] = sit9531x_prio_slot_get(val, slot);
+	}
+
+	return 0;
+}
+
+/*
+ * sit9531x_input_prio_set - move an input to a priority slot
+ * @input_idx:	input source in hardware encoding (0-11, see
+ *		sit9531x_input_hw_src())
+ * @prio:	priority slot position (0 = highest)
+ *
+ * Reads the PLL's table, takes the source out of wherever it sits and
+ * reinserts it at @prio, shifting the entries in between.  The rest keep
+ * their relative order: a priority change asks about one input, so the
+ * fallbacks configured behind it have to survive it.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, <0 on error
+ */
+int sit9531x_input_prio_set(struct sit9531x_dev *sitdev, u8 pll_idx,
+			    u8 input_idx, u8 prio)
+{
+	u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+	u8 slot, from;
+	int rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+	if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+		return -EINVAL;
+	if (prio >= SIT9531X_PRIO_MAX_SLOTS)
+		return -EINVAL;
+
+	rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+	if (rc)
+		return rc;
+
+	for (from = 0; from < SIT9531X_PRIO_MAX_SLOTS; from++)
+		if (srcs[from] == input_idx)
+			break;
+
+	if (from == prio)
+		return 0;
+
+	if (from > prio) {
+		/*
+		 * Moving up, or not in the table at all: push the tail down
+		 * to open the slot.  For a source that is new, whatever sat
+		 * in the last slot falls off -- the entry the chip would have
+		 * tried last.
+		 */
+		if (from == SIT9531X_PRIO_MAX_SLOTS)
+			from = SIT9531X_PRIO_MAX_SLOTS - 1;
+		for (slot = from; slot > prio; slot--)
+			srcs[slot] = srcs[slot - 1];
+	} else {
+		for (slot = from; slot < prio; slot++)
+			srcs[slot] = srcs[slot + 1];
+	}
+
+	srcs[prio] = input_idx;
+
+	return sit9531x_prio_table_commit(sitdev, pll_idx, srcs);
+}
+
+/*
+ * sit9531x_input_prio_remove - drop an input from a PLL's priority table
+ * @input_idx:	input source in hardware encoding
+ *
+ * Rewrites the priority table with the source removed: the remaining
+ * sources are compacted toward the highest-priority slots and the freed
+ * tail slots are backfilled with the lowest-priority remaining source
+ * (matching sit9531x_input_prio_set()).  This makes a disconnected
+ * input ineligible for automatic reference selection, not just gated at
+ * the input buffer.  It is a no-op when the source is absent, or when it
+ * is the only source (the buffer disable then drives the PLL to
+ * holdover and the table must not be left empty).
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_input_prio_remove(struct sit9531x_dev *sitdev, u8 pll_idx,
+			       u8 input_idx)
+{
+	u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+	u8 kept[SIT9531X_PRIO_MAX_SLOTS];
+	u8 slot, count = 0;
+	bool found = false;
+	int rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+	if (rc)
+		return rc;
+
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+		if (srcs[slot] == input_idx)
+			found = true;
+		else
+			kept[count++] = srcs[slot];
+	}
+
+	if (!found || count == 0)
+		return 0;
+
+	/* Backfill freed tail slots with the lowest-priority remaining src */
+	while (count < SIT9531X_PRIO_MAX_SLOTS) {
+		kept[count] = kept[count - 1];
+		count++;
+	}
+
+	return sit9531x_prio_table_commit(sitdev, pll_idx, kept);
+}
+
+/*
+ * sit9531x_input_prio_add - make an input eligible in a PLL's table
+ * @input_idx:	input source in hardware encoding
+ *
+ * Ensures the source appears in the priority table so it can be picked
+ * by automatic reference selection again after a disconnect.  If the
+ * source is already listed the table is left untouched; otherwise it is
+ * placed in the lowest-priority slot.  The original priority is not
+ * restored -- use sit9531x_input_prio_set() to reassign it.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
+			    u8 input_idx)
+{
+	u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+	u8 slot;
+	int rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+	if (rc)
+		return rc;
+
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++)
+		if (srcs[slot] == input_idx)
+			return 0;
+
+	srcs[SIT9531X_PRIO_MAX_SLOTS - 1] = input_idx;
+
+	return sit9531x_prio_table_commit(sitdev, pll_idx, srcs);
+}
+
+/* Per-slot DIVO base register offsets (6 slots per page) */
+static const u8 clkout_odr_divn_base[] = {
+	0x14, 0x24, 0x34, 0x44, 0x54, 0x64
+};
+
+/* XO doubler register */
+#define SIT9531X_REG_XO2_GENERIC		SIT9531X_REG(0x00, 0x2D)
+#define SIT9531X_XO_DOUBLER_ENB_BIT		7   /* inverted: 0 = enabled */
+
+/* VCO frequency bands (Hz) */
+#define SIT9531X_FVCO_LOWBAND_MIN		4915200000ULL
+#define SIT9531X_FVCO_LOWBAND_MAX		5898240000ULL
+#define SIT9531X_FVCO_HIGHBAND_MIN		6875000000ULL
+#define SIT9531X_FVCO_HIGHBAND_MAX		7812500000ULL
+
+/*
+ * sit9531x_is_xo_doubler_enabled - check if Fref doubler is active
+ *
+ * Register 0x2D bit 7 is active-low: 0 = doubler enabled, 1 = disabled.
+ *
+ * Return: 1 if enabled, 0 if disabled, <0 on error
+ */
+static int sit9531x_is_xo_doubler_enabled(struct sit9531x_dev *sitdev)
+{
+	u8 val;
+	int rc;
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_XO2_GENERIC, &val);
+	if (rc)
+		return rc;
+
+	return (~val >> SIT9531X_XO_DOUBLER_ENB_BIT) & 1u;
+}
+
+/*
+ * sit9531x_dbg_sample - latch and read a signal pathway debug sample
+ * @sitdev:	device pointer
+ * @pll_idx:	PLL index (0-3)
+ * @read_code:	which tap of the pathway to sample
+ * @buf:	result, least significant byte first
+ * @len:	bytes to read, at most SIT9531X_DBG_DATA_BYTES
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int sit9531x_dbg_sample(struct sit9531x_dev *sitdev, u8 pll_idx,
+			       u8 read_code, u8 *buf, unsigned int len)
+{
+	unsigned int i;
+	int rc;
+	u8 v;
+
+	if (len > SIT9531X_DBG_DATA_BYTES)
+		return -EINVAL;
+
+	rc = sit9531x_write_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_DEBUG,
+				   SIT9531X_PLL_DEBUG_UNLOCK);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+				   SIT9531X_PLL_REG_DBG_READ_CODE, read_code);
+	if (rc)
+		return rc;
+
+	/* Reading the trigger latches a fresh sample of the selected tap. */
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_TRIGGER, &v);
+	if (rc)
+		return rc;
+
+	for (i = 0; i < len; i++) {
+		rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+					  SIT9531X_PLL_REG_DBG_DATA_0 + i,
+					  &buf[i]);
+		if (rc)
+			return rc;
+	}
+
+	return 0;
+}
+
+/*
+ * DIVN as a fixed-point value: int_part plus fracn/fracd, carried with
+ * SIT9531X_DIVN_SCALE steps per unit.  The scale keeps a whole DIVN
+ * well inside s64 while resolving far below the parts-per-trillion the
+ * frequency offset is reported in.
+ */
+static s64 sit9531x_divn_fixed(u32 int_part, s64 fracn, u64 fracd)
+{
+	s64 whole = (s64)int_part * SIT9531X_DIVN_SCALE;
+	u64 frac;
+
+	if (!fracd)
+		return whole;
+
+	frac = mul_u64_u64_div_u64(abs(fracn), SIT9531X_DIVN_SCALE, fracd);
+
+	return fracn < 0 ? whole - (s64)frac : whole + (s64)frac;
+}
+
+/*
+ * sit9531x_divn_static - read the configured DIVN of a PLL
+ * @sitdev:	device pointer
+ * @pll_idx:	PLL index (0-3)
+ * @divn:	result, fixed point as per sit9531x_divn_fixed()
+ *
+ * Reads PLL page regs 0x30 (integer part), 0x32-0x35 (numerator) and
+ * 0x38-0x3B (denominator).  The numerator is a two's complement 32-bit
+ * value, so DIVN can sit below the integer part, and the denominator
+ * register holds the divisor minus one.
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int sit9531x_divn_static(struct sit9531x_dev *sitdev, u8 pll_idx,
+				s64 *divn)
+{
+	u32 int_part, fracn_raw = 0, fracd_raw = 0;
+	int rc, i;
+	u8 v;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DIVN_INT, &v);
+	if (rc)
+		return rc;
+	int_part = v;
+
+	for (i = 3; i >= 0; i--) {
+		rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+					  SIT9531X_PLL_REG_DIVN_NUM + i, &v);
+		if (rc)
+			return rc;
+		fracn_raw = (fracn_raw << 8) | v;
+	}
+
+	for (i = 3; i >= 0; i--) {
+		rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+					  SIT9531X_PLL_REG_DIVN_DEN + i, &v);
+		if (rc)
+			return rc;
+		fracd_raw = (fracd_raw << 8) | v;
+	}
+
+	*divn = sit9531x_divn_fixed(int_part, (s32)fracn_raw,
+				    (u64)fracd_raw + 1);
+
+	return 0;
+}
+
+/*
+ * sit9531x_divn_runtime - read the DIVN the digital loop is commanding
+ * @sitdev:	device pointer
+ * @pll_idx:	PLL index (0-3)
+ * @divn:	result, fixed point as per sit9531x_divn_fixed()
+ *
+ * Same quantity as sit9531x_divn_static(), but sampled from the running
+ * loop rather than from the configuration registers, and carried at a
+ * wider precision: the numerator is 48 bits, two's complement, the
+ * denominator 49.  The integer part shares its tap with the numerator.
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int sit9531x_divn_runtime(struct sit9531x_dev *sitdev, u8 pll_idx,
+				 s64 *divn)
+{
+	u8 buf[SIT9531X_DBG_DATA_BYTES];
+	u64 fracn_raw = 0, fracd = 0;
+	u32 int_part;
+	int rc, i;
+
+	rc = sit9531x_dbg_sample(sitdev, pll_idx, SIT9531X_DBG_READ_CODE_DIVN,
+				 buf, SIT9531X_DBG_DATA_BYTES);
+	if (rc)
+		return rc;
+
+	for (i = 5; i >= 0; i--)
+		fracn_raw = (fracn_raw << 8) | buf[i];
+
+	int_part = buf[6] | ((u32)(buf[7] & SIT9531X_DIVN_RT_INT_HI_BIT) << 8);
+
+	rc = sit9531x_dbg_sample(sitdev, pll_idx,
+				 SIT9531X_DBG_READ_CODE_DIVN_DEN, buf,
+				 SIT9531X_DBG_DATA_BYTES);
+	if (rc)
+		return rc;
+
+	for (i = 5; i >= 0; i--)
+		fracd = (fracd << 8) | buf[i];
+
+	fracd |= (u64)(buf[6] & SIT9531X_DIVN_RT_INT_HI_BIT) << 48;
+
+	*divn = sit9531x_divn_fixed(int_part,
+				    sign_extend64(fracn_raw,
+						  SIT9531X_DIVN_RT_NUM_BITS - 1),
+				    fracd);
+
+	return 0;
+}
+
+/**
+ * sit9531x_pll_ffo_ppt - fractional frequency offset of a PLL's reference
+ * @sitdev:	device pointer
+ * @pll_idx:	PLL index (0-3)
+ * @ffo:	result in parts per trillion
+ *
+ * A locked PLL commands whatever DIVN keeps its VCO tracking the
+ * reference.  How far that sits from the configured DIVN is how far the
+ * reference sits from the local oscillator, which is the fractional
+ * frequency offset the DPLL ABI reports for the pin feeding the device.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, -ENODATA when DIVN is not programmed, <0 on
+ * error.
+ */
+int sit9531x_pll_ffo_ppt(struct sit9531x_dev *sitdev, u8 pll_idx, s64 *ffo)
+{
+	s64 configured, running, delta;
+	u64 magnitude;
+	int rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	rc = sit9531x_divn_static(sitdev, pll_idx, &configured);
+	if (rc)
+		return rc;
+	if (configured <= 0)
+		return -ENODATA;
+
+	rc = sit9531x_divn_runtime(sitdev, pll_idx, &running);
+	if (rc)
+		return rc;
+
+	delta = running - configured;
+	magnitude = mul_u64_u64_div_u64(abs(delta), SIT9531X_PPT_PER_UNIT,
+					(u64)configured);
+
+	*ffo = delta < 0 ? -(s64)magnitude : (s64)magnitude;
+
+	return 0;
+}
+
+/*
+ * sit9531x_get_fvco - read VCO frequency from chip's DIVN registers
+ *
+ * Fvco = Fref * DIVN, where DIVN comes from sit9531x_divn_static() and
+ * Fref = xtal_freq << doubler.  DIVN is the
+ * steady-state Fvco/Fref target programmed by the NVM blob and is
+ * authoritative in both free-run and sync modes; the previous split
+ * between free-run and sync formulas returned 0 on chips that didn't
+ * have a sync input populated, which broke the TDC phase readback.
+ *
+ * Return: Fvco in Hz, or 0 on error
+ */
+static u64 sit9531x_get_fvco(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+	int doubler, rc;
+	s64 divn;
+	u64 fref;
+
+	/*
+	 * DT board-config override: some configs (e.g. an INTSYNC PLL)
+	 * run a VCO that Fref*DIVN does not reproduce.  When the board
+	 * supplies the measured VCO, use it verbatim.
+	 */
+	if (pll_idx < SIT9531X_NUM_PLLS && sitdev->pll_fvco[pll_idx])
+		return sitdev->pll_fvco[pll_idx];
+
+	rc = sit9531x_divn_static(sitdev, pll_idx, &divn);
+	if (rc || divn <= 0)
+		return 0;
+
+	doubler = sit9531x_is_xo_doubler_enabled(sitdev);
+	if (doubler < 0)
+		return 0;
+
+	fref = (u64)sitdev->xtal_freq << doubler;
+
+	return mul_u64_u64_div_u64(fref, (u64)divn, SIT9531X_DIVN_SCALE);
+}
+
+/*
+ * sit9531x_output_phase_flush - flush the output phase of a PLL
+ *
+ * Fires the chip's on-demand phase-flush (PHFL) so every output divider
+ * of @pll_idx restarts aligned to the PLL phase.  Without it a rewritten
+ * DIVO keeps counting from an arbitrary point and the output edge lands
+ * with a persistent offset against the tracked reference (only a power
+ * cycle realigned it).
+ *
+ * The sequence mirrors the vendor procedure: arm the on-demand PHFL and
+ * latch it with the PLL-page small-change update, then select the
+ * in-register phase trigger on Page 0 and pulse it.  The Page 0 trigger
+ * register is touched read-modify-write so the unrelated OEb bits are
+ * preserved.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_output_phase_flush(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+	u8 ctrl, orig;
+	int rc, ret;
+
+	/* Arm the on-demand phase-flush on the PLL page. */
+	rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+				    SIT9531X_PLL_REG_PHFL_CTRL,
+				    SIT9531X_PLL_PHFL_ON_DEMAND_EN,
+				    SIT9531X_PLL_PHFL_ON_DEMAND_EN);
+	if (rc)
+		return rc;
+
+	/* Latch it with the PLL small-change update. */
+	rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+				    SIT9531X_PLL_REG_SMALL_UPDATE,
+				    SIT9531X_SMALL_UPDATE_CMD,
+				    SIT9531X_SMALL_UPDATE_CMD);
+	if (rc)
+		return rc;
+
+	/*
+	 * Select the in-register phase trigger, preserving the OEb bits.
+	 * Remember the original register value (with the trigger de-asserted)
+	 * so the trigger-source select can be restored once the pulse has
+	 * fired.
+	 */
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, &ctrl);
+	if (rc)
+		return rc;
+
+	orig = ctrl & ~SIT9531X_DIVO_PHASE_TRIG;
+	ctrl = orig | SIT9531X_DIVO_PHASE_SEL_REG;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, ctrl);
+	if (rc)
+		return rc;
+
+	/*
+	 * Pulse the phase trigger.  No explicit delay is needed between the
+	 * set and clear writes: each I2C transaction takes far longer than
+	 * any minimum pulse width.
+	 */
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1,
+			       ctrl | SIT9531X_DIVO_PHASE_TRIG);
+
+	/*
+	 * Restore the original trigger-source select.  The pulse above has
+	 * already latched the flush, so a one-shot flush must not leave the
+	 * phase trigger permanently pinned to the in-register source.  This
+	 * runs even when the pulse write failed, otherwise a failed flush
+	 * would keep a hardware trigger source hijacked; the restore error
+	 * is only surfaced when it would not mask the pulse failure.
+	 */
+	ret = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, orig);
+	if (ret && !rc)
+		rc = ret;
+
+	return rc;
+}
+
+/*
+ * sit9531x_output_freq_set - set output clock frequency
+ * @out_idx:	output index (0-N for this chip variant)
+ * @pll_idx:	PLL driving this output (0-3)
+ * @frequency:	desired output frequency in Hz
+ *
+ * Computes DIVO = Fvco / frequency and writes the 34-bit output divider
+ * to the output system registers on Pages 3/4.  The write sequence is:
+ *   1. Unlock debug registers (Page 3)
+ *   2. Enter PRG_CMD state
+ *   3. Write 5-byte DIVO to the correct page/slot
+ *   4. NVM update
+ *   5. Loop lock
+ *   6. Wait for lock to settle
+ *   7. Flush the output phase so the new divider starts aligned
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, <0 on error.  Actual frequency may differ
+ *         due to integer division; the output state is updated with
+ *         the effective frequency (Fvco / DIVO).
+ */
+int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
+			     u8 pll_idx, u64 frequency)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	u8 slot, page, base_reg, divo_bytes[5], msb_old;
+	u64 fvco, divo, fvco_min, fvco_max;
+	int rc, j, ret;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (out_idx >= info->num_outputs || pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	if (!frequency)
+		return -EINVAL;
+
+	/* Determine VCO frequency band limits */
+	if (pll_idx == 1 || pll_idx == 3) {
+		/* PLLB, PLLD: high band */
+		fvco_min = SIT9531X_FVCO_HIGHBAND_MIN;
+		fvco_max = SIT9531X_FVCO_HIGHBAND_MAX;
+	} else {
+		/* PLLA, PLLC: low band */
+		fvco_min = SIT9531X_FVCO_LOWBAND_MIN;
+		fvco_max = SIT9531X_FVCO_LOWBAND_MAX;
+	}
+
+	/*
+	 * Read current VCO frequency.  When the board supplies an explicit
+	 * Fvco via "sitime,pll-fvco" the override is the source of truth
+	 * (e.g. a chip variant that runs out of the documented band, or a
+	 * mode like INTSYNC where Fref*DIVN does not reproduce the VCO), so
+	 * skip the band clamp in that case.
+	 */
+	fvco = sit9531x_get_fvco(sitdev, pll_idx);
+	if (!fvco) {
+		fvco = fvco_min;
+	} else if (!sitdev->pll_fvco[pll_idx]) {
+		if (fvco < fvco_min)
+			fvco = fvco_min;
+		else if (fvco > fvco_max)
+			fvco = fvco_max;
+	}
+
+	divo = div64_u64(fvco, frequency);
+	if (!divo)
+		return -EINVAL;
+
+	dev_dbg(sitdev->dev,
+		"out%u: Fvco=%llu freq=%llu DIVO=%llu (effective %llu Hz)\n",
+		out_idx, fvco, frequency, divo, div64_u64(fvco, divo));
+
+	/* Map output index to physical slot */
+	slot = info->clkout_map[out_idx];
+
+	/* Determine page and per-page slot register */
+	if (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX)
+		page = SIT9531X_PAGE_OUTSYS1;
+	else
+		page = SIT9531X_PAGE_OUTSYS0;
+	base_reg = clkout_odr_divn_base[slot % 6];
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
+			       SIT9531X_DEBUG_UNLOCK_VAL);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+			       SIT9531X_PRG_CMD_STATE);
+	if (rc)
+		return rc;
+
+	divo_bytes[0] = (divo >>  0) & 0xFF;
+	divo_bytes[1] = (divo >>  8) & 0xFF;
+	divo_bytes[2] = (divo >> 16) & 0xFF;
+	divo_bytes[3] = (divo >> 24) & 0xFF;
+	divo_bytes[4] = (divo >> 32) & 0x03;  /* only bits [1:0] */
+
+	rc = sit9531x_read_u8(sitdev,
+			      SIT9531X_REG(page, base_reg - 4), &msb_old);
+	if (rc)
+		goto commit;
+	divo_bytes[4] |= msb_old & 0xFC;
+
+	for (j = 0; j < 5; j++) {
+		rc = sit9531x_write_u8(sitdev,
+				       SIT9531X_REG(page, base_reg - j),
+				       divo_bytes[j]);
+		if (rc)
+			goto commit;
+	}
+
+commit:
+	/*
+	 * Step 4: NVM update + loop lock.  Always run prg_commit() so the chip
+	 * leaves the PRG_CMD state with the output loops re-locked, even when a
+	 * write above failed; keep the first error to return.  It also carries
+	 * the required post-lock settling sleep.
+	 */
+	ret = sit9531x_prg_commit(sitdev);
+	if (ret && !rc)
+		rc = ret;
+	if (rc)
+		return rc;
+
+	/*
+	 * Step 5: flush the PLL's output phase so the new DIVO starts
+	 * aligned instead of keeping the arbitrary phase the divider
+	 * happened to be at.
+	 */
+	rc = sit9531x_output_phase_flush(sitdev, pll_idx);
+	if (rc)
+		return rc;
+
+	sitdev->out[out_idx].freq = (u32)div64_u64(fvco, divo);
+
+	return 0;
+}
+
+/*
+ * sit9531x_output_freq_get - read output clock frequency from hardware
+ * @out_idx:	output index (0-N for this chip variant)
+ * @frequency:	output frequency in Hz
+ *
+ * Reads the 34-bit DIVO divider back from the output system registers
+ * and computes the live output frequency as Fvco / DIVO.  This stays
+ * correct even when the divider was reprogrammed behind the driver's
+ * back (e.g. by a direct-I2C userspace tool), where the cached value
+ * would be stale.
+ *
+ * The cached output state is refreshed with the computed value.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, -ENODEV when the output divider or VCO rate
+ *	   is not resolvable, <0 on register access error
+ */
+int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
+			     u64 *frequency)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	u8 slot, page, base_reg, pll_idx, v;
+	u64 fvco, divo = 0;
+	int rc, j;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (out_idx >= info->num_outputs)
+		return -EINVAL;
+
+	pll_idx = sitdev->out[out_idx].pll_idx;
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -ENODEV;
+
+	fvco = sit9531x_get_fvco(sitdev, pll_idx);
+	if (!fvco)
+		return -ENODEV;
+
+	slot = info->clkout_map[out_idx];
+	if (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX)
+		page = SIT9531X_PAGE_OUTSYS1;
+	else
+		page = SIT9531X_PAGE_OUTSYS0;
+	base_reg = clkout_odr_divn_base[slot % 6];
+
+	for (j = 4; j >= 0; j--) {
+		rc = sit9531x_read_u8(sitdev,
+				      SIT9531X_REG(page, base_reg - j), &v);
+		if (rc)
+			return rc;
+		if (j == 4)
+			v &= 0x03;
+		divo = (divo << 8) | v;
+	}
+
+	if (!divo)
+		return -ENODEV;
+
+	*frequency = div64_u64(fvco, divo);
+	sitdev->out[out_idx].freq = (u32)*frequency;
+
+	return 0;
+}
+
+/*
+ * Phase adjust (PRG_RST_DELAY register-based).
+ *
+ * The chip exposes a per-output 34-bit coarse delay measured in VCO
+ * clock periods plus a 3-bit fine delay in fixed 30 ps steps.  The
+ * five bytes PROG6..PROG2 hold the field across registers:
+ *   base + 0  PROG6  [7:5] OPSTG_VCASC_BUMP (preserved via RMW)
+ *                    [4:2] PRG_RST_FINE_DELAY
+ *                    [1:0] PRG_RST_DELAY[33:32]
+ *   base + 1  PROG5  PRG_RST_DELAY[31:24]
+ *   base + 2  PROG4  PRG_RST_DELAY[23:16]
+ *   base + 3  PROG3  PRG_RST_DELAY[15:8]
+ *   base + 4  PROG2  PRG_RST_DELAY[7:0]
+ *
+ * Outputs 0-5 live on Page 3, outputs 6-11 on Page 4, with each
+ * output's block at base = 0x15 + 16 * (out_idx % 6).
+ *
+ * The chip only supports unsigned positive delay.  A negative phase
+ * adjustment (advance) is wrapped to (T_out - |phase|) modulo one
+ * output period, which is identical for a periodic signal.
+ */
+
+int sit9531x_output_phase_adjust_set(struct sit9531x_dev *sitdev,
+				     u8 out_idx, s32 phase_ps)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	u64 abs_ps, fvco, coarse, coarse_ps, rem_ps;
+	u8 page, base, prog6_val, fine = 0;
+	u8 pll_idx, slot;
+	u32 freq;
+	int rc, ret;
+
+	if (out_idx >= info->num_outputs)
+		return -EINVAL;
+
+	pll_idx = sitdev->out[out_idx].pll_idx;
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	freq = sitdev->out[out_idx].freq;
+	if (!freq)
+		return -EINVAL;
+
+	fvco = sit9531x_get_fvco(sitdev, pll_idx);
+	if (!fvco)
+		return -EIO;
+
+	/*
+	 * Convert to unsigned absolute delay.  Negative phase (advance)
+	 * is rendered as T_out - |phase|, modulo the output period.
+	 */
+	if (phase_ps == 0) {
+		abs_ps = 0;
+	} else if (phase_ps > 0) {
+		abs_ps = (u64)phase_ps;
+	} else {
+		u64 t_out_ps = div64_u64(1000000000000ULL, freq);
+		u64 advance = (u64)(-(s64)phase_ps);
+
+		if (t_out_ps == 0)
+			return -EINVAL;
+		advance %= t_out_ps;
+		abs_ps = (advance == 0) ? 0 : (t_out_ps - advance);
+	}
+
+	/*
+	 * coarse_cycles = abs_ps * Fvco / 1e12 ps/s.
+	 * mul_u64_u64_div_u64() avoids overflow when abs_ps approaches
+	 * one second of 1 PPS wrap-around.
+	 */
+	coarse = mul_u64_u64_div_u64(abs_ps, fvco, 1000000000000ULL);
+	if (coarse >= (1ULL << SIT9531X_OUT_PRG_COARSE_BITS))
+		return -ERANGE;
+
+	/* Fine delay = round((abs_ps - coarse * vco_period_ps) / 30 ps) */
+	coarse_ps = mul_u64_u64_div_u64(coarse, 1000000000000ULL, fvco);
+	rem_ps = (abs_ps > coarse_ps) ? (abs_ps - coarse_ps) : 0;
+	if (rem_ps) {
+		u64 steps;
+
+		steps = div64_u64(rem_ps + SIT9531X_OUT_PRG_FINE_STEP_PS / 2,
+				  SIT9531X_OUT_PRG_FINE_STEP_PS);
+		if (steps > SIT9531X_OUT_PRG_FINE_MAX)
+			steps = SIT9531X_OUT_PRG_FINE_MAX;
+		fine = (u8)steps;
+	}
+
+	/*
+	 * Map logical output index to the chip's physical output slot.
+	 * On SiT95317 the eight logical outputs land on chip slots
+	 * {0, 3, 4, 5, 7, 8, 9, 11}; on SiT95316 the map is identity.
+	 * Page/base must address the slot, not the logical index.
+	 */
+	slot = info->clkout_map[out_idx];
+	page = (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX) ?
+	       SIT9531X_PAGE_OUTSYS1 : SIT9531X_PAGE_OUTSYS0;
+	base = SIT9531X_OUT_PRG_DELAY_BASE +
+	       SIT9531X_OUT_PRG_SLOT_STRIDE * (slot % 6);
+
+	/* Caller (dpll.c) holds multiop_lock around the whole sequence. */
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	/*
+	 * The PRG_RST_DELAY bytes live in the output system, so the writes
+	 * only take effect when made inside the PRG_CMD programming state and
+	 * committed to the NVM shadow, exactly like sit9531x_output_freq_set().
+	 */
+	rc = sit9531x_prg_enter(sitdev);
+	if (rc)
+		return rc;
+
+	/* PROG6 RMW: preserve OPSTG_VCASC_BUMP in [7:5] */
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG(page, base),
+			      &prog6_val);
+	if (rc)
+		goto commit;
+
+	prog6_val &= SIT9531X_OUT_PRG_OPSTG_MASK;
+	prog6_val |= (fine << SIT9531X_OUT_PRG_FINE_SHIFT) &
+		     SIT9531X_OUT_PRG_FINE_MASK;
+	prog6_val |= (u8)((coarse >> 32) & SIT9531X_OUT_PRG_COARSE_HI_MASK);
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base),
+			       prog6_val);
+	if (rc)
+		goto commit;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base + 1),
+			       (u8)((coarse >> 24) & 0xFF));
+	if (rc)
+		goto commit;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base + 2),
+			       (u8)((coarse >> 16) & 0xFF));
+	if (rc)
+		goto commit;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base + 3),
+			       (u8)((coarse >> 8) & 0xFF));
+	if (rc)
+		goto commit;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base + 4),
+			       (u8)(coarse & 0xFF));
+
+commit:
+	/*
+	 * Always leave the PRG_CMD state via prg_commit(), even on a
+	 * mid-sequence write failure, so the output loops are re-locked rather
+	 * than stranded unlocked; keep the first error.
+	 */
+	ret = sit9531x_prg_commit(sitdev);
+	if (ret && !rc)
+		rc = ret;
+	if (rc)
+		return rc;
+
+	/*
+	 * Restart the output divider phase so the freshly programmed delay is
+	 * applied against a known edge instead of the divider's arbitrary
+	 * running phase.
+	 */
+	return sit9531x_output_phase_flush(sitdev, pll_idx);
+}
+
+/*
+ * sit9531x_clear_notifications - clear all notification registers
+ *
+ * Clears all write-1-to-clear notification registers:
+ *   - PLL outer LOL notification (Page 0, reg 0x07)
+ *   - PLL holdover freeze notification (Page 0, reg 0x0B)
+ *   - PLL inner LOL notification (Page 0, reg 0x93)
+ *   - Clock monitor XO/PLL notification (Page 0, reg 0x9E)
+ *   - Clock input notifications (Page 6, regs 0x03/0x07/0x93/0x97)
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
+{
+	int rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	/* Page 0x00 W1C notification registers */
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_OUTER_LOL_NOTIF, 0xFF);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_HO_FREEZE_NOTIF, 0xFF);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PLL_INNER_LOL_NOTIF, 0xFF);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_CMON_NOTIF, 0xFF);
+	if (rc)
+		return rc;
+
+	/* Page 0x06 clock input monitor notifications */
+	rc = sit9531x_write_u8(sitdev, SIT9531X_CLKMON_P_NOTIF_01, 0xFF);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_CLKMON_P_NOTIF_23, 0xFF);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_CLKMON_N_NOTIF_01, 0xFF);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_CLKMON_N_NOTIF_23, 0xFF);
+	if (rc)
+		return rc;
+
+	dev_dbg(sitdev->dev, "All notification registers cleared\n");
+	return 0;
+}
+
+/*
+ * INTSYNC configuration register values.
+ * These are written to the source PLL's EXT page to enable/disable
+ * inter-PLL synchronization (lock frequency PLL to phase PLL).
+ */
+struct sit9531x_intsync_reg {
+	u8 offset;
+	u8 en_val;
+	u8 dis_val;
+};
+
+static const struct sit9531x_intsync_reg intsync_config[] = {
+	{ 0x2D, 0x02, 0x00 },
+	{ 0x50, 0x08, 0x00 },
+	{ 0x51, 0x04, 0x00 },
+	{ 0x54, 0x02, 0x00 },
+	{ 0x55, 0x28, 0x20 },
+	{ 0x5C, 0x0F, 0x00 },
+	{ 0x5D, 0xFF, 0x00 },
+	{ 0x6C, 0xDD, 0x00 },
+};
+
+/*
+ * sit9531x_intsync_enable - enable inter-PLL synchronization
+ * @src_pll_idx: source (frequency) PLL index (0-3)
+ *
+ * Enables INTSYNC global bit, unlocks the source PLL's EXT page
+ * debug registers, writes configuration, and triggers a small
+ * update on the source PLL.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_intsync_enable(struct sit9531x_dev *sitdev, u8 src_pll_idx)
+{
+	u8 ext_page, val;
+	int rc, i;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (src_pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	ext_page = SIT9531X_PLL_EXT_PAGE(src_pll_idx);
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &val);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL,
+			       val | BIT(SIT9531X_INTSYNC_EN_BIT));
+	if (rc)
+		return rc;
+
+	/* Small update on Page 0 */
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+			       SIT9531X_SMALL_UPDATE_CMD);
+	if (rc)
+		return rc;
+
+	/* Unlock debug on EXT page */
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(ext_page, SIT9531X_PLL_REG_DEBUG),
+			       SIT9531X_PLL_DEBUG_UNLOCK);
+	if (rc)
+		return rc;
+
+	for (i = 0; i < ARRAY_SIZE(intsync_config); i++) {
+		rc = sit9531x_write_u8(sitdev,
+				       SIT9531X_REG(ext_page,
+						    intsync_config[i].offset),
+				       intsync_config[i].en_val);
+		if (rc)
+			return rc;
+	}
+
+	/* Small update on source PLL */
+	rc = sit9531x_write_pll_u8(sitdev, src_pll_idx,
+				   SIT9531X_PLL_REG_SMALL_UPDATE,
+				   SIT9531X_SMALL_UPDATE_CMD);
+	if (rc)
+		return rc;
+
+	return 0;
+}
+
+/*
+ * sit9531x_intsync_disable - disable inter-PLL synchronization
+ * @src_pll_idx: source (frequency) PLL index (0-3)
+ *
+ * Clears INTSYNC global bit, writes disable values to the source
+ * PLL's EXT page, and triggers a small update.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_intsync_disable(struct sit9531x_dev *sitdev, u8 src_pll_idx)
+{
+	u8 ext_page, val;
+	int rc, i;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (src_pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	ext_page = SIT9531X_PLL_EXT_PAGE(src_pll_idx);
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &val);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL,
+			       val & ~BIT(SIT9531X_INTSYNC_EN_BIT));
+	if (rc)
+		return rc;
+
+	/* Small update on Page 0 */
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+			       SIT9531X_SMALL_UPDATE_CMD);
+	if (rc)
+		return rc;
+
+	/* Unlock debug on EXT page */
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(ext_page, SIT9531X_PLL_REG_DEBUG),
+			       SIT9531X_PLL_DEBUG_UNLOCK);
+	if (rc)
+		return rc;
+
+	for (i = 0; i < ARRAY_SIZE(intsync_config); i++) {
+		rc = sit9531x_write_u8(sitdev,
+				       SIT9531X_REG(ext_page,
+						    intsync_config[i].offset),
+				       intsync_config[i].dis_val);
+		if (rc)
+			return rc;
+	}
+
+	/* Small update on source PLL */
+	rc = sit9531x_write_pll_u8(sitdev, src_pll_idx,
+				   SIT9531X_PLL_REG_SMALL_UPDATE,
+				   SIT9531X_SMALL_UPDATE_CMD);
+	if (rc)
+		return rc;
+
+	return 0;
+}
+
+/*
+ * sit9531x_output_pulse_ctrl_set - program per-output PULSE_CTRL byte
+ * @out_idx:	logical output index (translated to chip slot internally)
+ * @pulse_ctrl:	8-bit PULSE_CTRL value (PROG0)
+ *
+ * Writes ODRn_PROG0 on the output page (Page 3 for slots 0..5,
+ * Page 4 for slots 6..11) at offset 0x1B + 16 * (slot % 6).
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_output_pulse_ctrl_set(struct sit9531x_dev *sitdev,
+				   u8 out_idx, u8 pulse_ctrl)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	u8 slot, page, reg;
+	int rc, ret;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (out_idx >= info->num_outputs)
+		return -EINVAL;
+
+	slot = info->clkout_map[out_idx];
+	page = (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX) ?
+		SIT9531X_PAGE_OUTSYS1 : SIT9531X_PAGE_OUTSYS0;
+	reg = SIT9531X_OUT_PROG0_BASE +
+	      SIT9531X_OUT_PRG_SLOT_STRIDE * (slot % 6);
+
+	/*
+	 * PROG0 lives in the output system, so like the DIVO and
+	 * PRG_RST_DELAY writes it only takes effect inside the PRG_CMD
+	 * programming state committed to the NVM shadow.
+	 */
+	rc = sit9531x_prg_enter(sitdev);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, reg), pulse_ctrl);
+
+	/*
+	 * Always leave the PRG_CMD state via prg_commit(), even if the write
+	 * failed, so the output loops are re-locked rather than stranded
+	 * unlocked; keep the first error.
+	 */
+	ret = sit9531x_prg_commit(sitdev);
+	if (ret && !rc)
+		rc = ret;
+
+	return rc;
+}
+
+/* Retries (at 1 s each) to wait for a PLL to reach its active state. */
+#define SIT9531X_PLL_ACTIVE_RETRIES	90
+
+/*
+ * sit9531x_phase_offset_read - read phase difference via TDC
+ * @phase_ps:	output phase difference in picoseconds
+ *
+ * Reads the Time-to-Digital Converter (TDC) 40-bit code from the
+ * PLL page registers, then converts to picoseconds using the VCO
+ * frequency: phase_diff = tdc_code / fvco.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_phase_offset_read(struct sit9531x_dev *sitdev, u8 pll_idx,
+			       s64 *phase_ps)
+{
+	u64 fvco, fvco_mhz;
+	s64 tdc_signed;
+	u64 tdc_raw;
+	int rc, i;
+	bool sign;
+	u8 v;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	/* Unlock the debug page so the TDC registers are accessible. */
+	rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+				   SIT9531X_PLL_REG_DEBUG,
+				   SIT9531X_PLL_DEBUG_UNLOCK);
+	if (rc)
+		return rc;
+
+	/*
+	 * Select the debug clock for taps below 200 kHz, then point the
+	 * readback at the TDC.  Only the one bit is touched: writing the
+	 * modifier register whole would clear the fields belonging to
+	 * other taps.
+	 */
+	rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+				    SIT9531X_PLL_REG_DBG_WRITE_CODE,
+				    SIT9531X_DBG_LOW_FREQ_CLK_BIT,
+				    SIT9531X_DBG_LOW_FREQ_CLK_BIT);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+				   SIT9531X_PLL_REG_DBG_READ_CODE,
+				   SIT9531X_DBG_READ_CODE_TDC);
+	if (rc)
+		return rc;
+
+	/* Latch a sample by reading the trigger register 3 times. */
+	for (i = 0; i < 3; i++) {
+		rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+					  SIT9531X_PLL_REG_DBG_TRIGGER, &v);
+		if (rc)
+			return rc;
+	}
+
+	tdc_raw = 0;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_4, &v);
+	if (rc)
+		return rc;
+	sign = !!(v & BIT(SIT9531X_TDC_SIGN_BIT));
+	tdc_raw = (u64)(v & 0x07) << 32;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_3, &v);
+	if (rc)
+		return rc;
+	tdc_raw |= (u64)v << 24;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_2, &v);
+	if (rc)
+		return rc;
+	tdc_raw |= (u64)v << 16;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_1, &v);
+	if (rc)
+		return rc;
+	tdc_raw |= (u64)v << 8;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_0, &v);
+	if (rc)
+		return rc;
+	tdc_raw |= v;
+
+	/*
+	 * Apply sign.  Per the vendor reference the sign bit is active-high
+	 * for a positive offset: bit set -> +code, bit clear -> -code.
+	 */
+	tdc_signed = sign ? (s64)tdc_raw : -(s64)tdc_raw;
+
+	/*
+	 * Get VCO frequency for conversion.  Fvco==0 means DIVN is not
+	 * programmed (PLL unused on this board) -- skip silently rather
+	 * than spamming the log on every poll cycle.
+	 */
+	fvco = sit9531x_get_fvco(sitdev, pll_idx);
+	if (!fvco) {
+		dev_dbg(sitdev->dev, "PLL%c: Fvco unknown, skip TDC\n",
+			'A' + pll_idx);
+		return -ENODEV;
+	}
+
+	/*
+	 * phase_diff (seconds) = tdc_code / fvco
+	 * phase_diff (ps) = tdc_code * 1e12 / fvco
+	 *
+	 * To avoid 64-bit overflow:
+	 *   phase_ps = tdc_code * 1e6 / (fvco / 1e6)
+	 */
+	fvco_mhz = div64_u64(fvco, 1000000ULL);
+	if (!fvco_mhz)
+		return -EIO;
+
+	*phase_ps = div64_s64(tdc_signed * 1000000LL, (s64)fvco_mhz);
+
+	return 0;
+}
+
+/*
+ * sit9531x_ref_state_fetch - read input reference status from hardware
+ * @index:	logical input index
+ *
+ * Reads LOS and OOF status for the given input lane from the Page 0x06
+ * clock monitor registers.  P and N lanes have separate register banks;
+ * each register carries two input pairs nibble-packed (even pair in
+ * bits [3:0], odd pair in [7:4]).
+ */
+static int sit9531x_ref_state_fetch(struct sit9531x_dev *sitdev, u8 index)
+{
+	unsigned int reg, force_reg, state_reg;
+	u8 pair, status, force, state;
+	struct sit9531x_ref *ref;
+	int rc;
+
+	/*
+	 * The XTAL/XO reference (index SIT9531X_MAX_INPUTS) is the on-chip
+	 * oscillator that feeds every PLL.  It cannot be routed or deselected,
+	 * so its pin is modelled as permanently connected (see
+	 * sit9531x_dpll_xo_pin_ops) and its LOS/OOF flags are never consulted.
+	 * Only the routable per-lane inputs (0..num_inputs-1) are polled here.
+	 */
+	if (index >= SIT9531X_MAX_INPUTS)
+		return -EINVAL;
+
+	ref = &sitdev->ref[index];
+	pair = sit9531x_input_pair(index);
+
+	if (sit9531x_input_is_n(index))
+		reg = pair < 2 ? SIT9531X_CLKMON_N_STATUS_01
+			       : SIT9531X_CLKMON_N_STATUS_23;
+	else
+		reg = pair < 2 ? SIT9531X_CLKMON_P_STATUS_01
+			       : SIT9531X_CLKMON_P_STATUS_23;
+
+	rc = sit9531x_read_u8(sitdev, reg, &status);
+	if (rc)
+		return rc;
+
+	if (pair & 1)
+		status >>= 4;
+
+	ref->los = !!(status & (BIT(SIT9531X_CLKMON_CLK_LOSS) |
+				BIT(SIT9531X_CLKMON_CLK_LOSS_FD)));
+	ref->oof = !!(status & (BIT(SIT9531X_CLKMON_FREQ_FINE) |
+				BIT(SIT9531X_CLKMON_FREQ_COARSE)));
+
+	/*
+	 * Whether the receiver is on.  This has to come from the chip: it
+	 * is the loaded configuration that decides, and without reading it
+	 * back every input would look disabled until something called
+	 * sit9531x_input_enable().  A lane counts as disabled only while
+	 * the force bit overrides it to the off state; with the force bit
+	 * clear it follows the configuration, which is the enabled case.
+	 */
+	sit9531x_input_get_regs(sitdev, index, &force_reg, &state_reg);
+
+	rc = sit9531x_read_u8(sitdev, force_reg, &force);
+	if (rc)
+		return rc;
+	rc = sit9531x_read_u8(sitdev, state_reg, &state);
+	if (rc)
+		return rc;
+
+	ref->enabled = !((force & BIT(pair)) && !(state & BIT(pair)));
+
+	return 0;
+}
+
+/*
+ * sit9531x_input_mode_fetch - detect SE/DE configuration of an input pair
+ * @pair:	input pair number (0-3)
+ *
+ * Reads CLKINx_INPUT_MODE and stores the detected signal mode on both
+ * lanes of the pair.  A pair with neither SE lane enabled is running
+ * differential.
+ */
+static int sit9531x_input_mode_fetch(struct sit9531x_dev *sitdev, u8 pair)
+{
+	enum sit9531x_signal_mode sig_mode;
+	u8 mode;
+	int rc;
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_IN_MODE(pair), &mode);
+	if (rc)
+		return rc;
+
+	if (mode & (SIT9531X_IN_MODE_SE_P_EN | SIT9531X_IN_MODE_SE_N_EN))
+		sig_mode = SIT9531X_MODE_SE;
+	else
+		sig_mode = SIT9531X_MODE_DE;
+
+	sitdev->ref[pair * 2].sig_mode = sig_mode;
+	sitdev->ref[pair * 2 + 1].sig_mode = sig_mode;
+
+	dev_dbg(sitdev->dev, "CLKIN%u mode reg 0x%02x -> %s\n", pair, mode,
+		sig_mode == SIT9531X_MODE_DE ? "differential" : "single-ended");
+
+	return 0;
+}
+
+/*
+ * sit9531x_chan_state_fetch - read PLL channel status from hardware
+ *
+ * Reads lock status and mode from the PLL status register.
+ */
+/* Read the PLL active-state bit (PLL page reg 0x02 bit 0). */
+static int sit9531x_pll_is_active(struct sit9531x_dev *sitdev, u8 pll_idx,
+				  bool *active)
+{
+	u8 v;
+	int rc;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_ACTIVE, &v);
+	if (rc)
+		return rc;
+
+	*active = !!(v & SIT9531X_PLL_ACTIVE_BIT);
+
+	return 0;
+}
+
+static int sit9531x_chan_state_fetch(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+	u8 status, outer_lol, input_sel, inner_lol, ho_freeze, activesel_reg;
+	struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+	bool active;
+	int rc;
+
+	/*
+	 * Whether the PLL is running at all.  The loss-of-lock bit read
+	 * below is driven by the PLL itself, so on one the loaded
+	 * configuration leaves unused it simply stays clear and would
+	 * otherwise read as a lock.
+	 */
+	rc = sit9531x_pll_is_active(sitdev, pll_idx, &active);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_STATUS, &status);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_OUTER_LOL_STATUS,
+			      &outer_lol);
+	if (rc)
+		return rc;
+
+	/*
+	 * Read the input source the PLL has currently selected as its
+	 * active reference.  This lives in the low nibble of the last
+	 * register of the PLL's page-1 priority block (CLK_ACTIVESEL_PLL),
+	 * not on the PLL page -- PLL-page 0x29 is a config register.
+	 */
+	activesel_reg = SIT9531X_PRIO_BASE_REG +
+			SIT9531X_PRIO_REGS_PER_PLL * pll_idx +
+			SIT9531X_PRIO_ACTIVESEL_OFF;
+	rc = sit9531x_read_u8(sitdev,
+			      SIT9531X_REG(SIT9531X_PAGE_PRIOSYS,
+					   activesel_reg),
+			      &input_sel);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_PLL_INNER_LOL_STATUS,
+			      &inner_lol);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_HO_FREEZE_STATUS, &ho_freeze);
+	if (rc)
+		return rc;
+
+	/* STATUS_1_GENERIC reports loss of lock, so invert it. */
+	chan->active = active;
+	chan->locked = active && !(outer_lol & BIT(pll_idx));
+	chan->mode = !!(status & SIT9531X_PLL_STATUS_OUTER_DIS);
+	chan->selected_ref =
+		sit9531x_hw_src_input(input_sel & SIT9531X_PRIO_NIBBLE_MASK);
+	chan->inner_lol = !!(inner_lol & BIT(pll_idx));
+	chan->ho_freeze = !!(ho_freeze & BIT(pll_idx));
+
+	return 0;
+}
+
+/*
+ * sit9531x_out_state_fetch - read output status from hardware
+ *
+ * Reads the output PLL association from the PLL page output map
+ * registers into out->routed / out->pll_idx, and the current drive
+ * state from the Hi-Z force bits into out->enabled.  The two are
+ * separate: routing decides whether the output gets a DPLL pin at all,
+ * while a muted but routed output keeps its pin and reports
+ * DPLL_PIN_STATE_DISCONNECTED until it is un-muted.
+ */
+static int sit9531x_out_state_fetch(struct sit9531x_dev *sitdev, u8 index)
+{
+	struct sit9531x_out *out = &sitdev->out[index];
+	u8 map_lo, map_hi, slot;
+	int pll_idx;
+	bool muted;
+	int rc;
+
+	slot = sitdev->info->clkout_map[index];
+
+	rc = sit9531x_output_forced_hiz(sitdev, slot, &muted);
+	if (rc)
+		return rc;
+
+	/*
+	 * DT board-config override: the per-PLL OUTPUT_ENABLE bitmaps
+	 * (0x27/0x28) do not unambiguously express output->PLL routing on
+	 * every config (overlaps, and some outputs routed outside that
+	 * path).  When the board supplies an explicit map, trust it.
+	 */
+	if (sitdev->out_pll_map_valid) {
+		u8 m = sitdev->out_pll_map[index];
+
+		if (m < SIT9531X_NUM_PLLS) {
+			out->pll_idx = m;
+			out->routed = true;
+			out->enabled = !muted;
+		} else {
+			out->pll_idx = 0;
+			out->routed = false;
+			out->enabled = false;
+		}
+		return 0;
+	}
+
+	/*
+	 * The OUT_MAP_LO/HI bitmaps are indexed by the physical slot the
+	 * output occupies on the chip, not by the driver's logical output
+	 * index (translated above via the chip-info clkout_map[]: identity
+	 * on SiT95316, non-contiguous on SiT95317).
+	 *
+	 * Determine which PLL drives this output by checking each PLL's
+	 * output map registers (0x27 = slots 8-11, 0x28 = slots 0-7).
+	 */
+	for (pll_idx = 0; pll_idx < SIT9531X_NUM_PLLS; pll_idx++) {
+		rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+					  SIT9531X_PLL_REG_OUT_MAP_LO, &map_lo);
+		if (rc)
+			return rc;
+
+		rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+					  SIT9531X_PLL_REG_OUT_MAP_HI, &map_hi);
+		if (rc)
+			return rc;
+
+		if (slot < 8) {
+			if (map_lo & BIT(slot)) {
+				out->pll_idx = pll_idx;
+				out->routed = true;
+				out->enabled = !muted;
+				return 0;
+			}
+		} else {
+			if (map_hi & BIT(slot - 8)) {
+				out->pll_idx = pll_idx;
+				out->routed = true;
+				out->enabled = !muted;
+				return 0;
+			}
+		}
+	}
+
+	/* Output not mapped to any PLL */
+	out->pll_idx = 0;
+	out->routed = false;
+	out->enabled = false;
+
+	return 0;
+}
+
+/*
+ * sit9531x_ref_pll_mask_fetch - seed the input-to-PLL usage masks
+ *
+ * ref->pll_mask is the refcount the disconnect path uses to decide when
+ * an input receiver may be powered down: the physical input is only
+ * disabled once the last DPLL has released it.  It therefore has to
+ * start out matching the hardware.  Without this pass every mask starts
+ * at zero, and disconnecting an input from one DPLL drops the mask to
+ * zero and disables a receiver the other DPLLs are still locked to.
+ *
+ * An input is counted for a PLL when it appears in that PLL's Page-1
+ * priority table, which is exactly the condition the connect and
+ * disconnect callbacks maintain.  Sources that are not physical inputs
+ * (OCXO, INTSYNC) and reserved codes are skipped.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_ref_pll_mask_fetch(struct sit9531x_dev *sitdev)
+{
+	u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+	u8 pll_idx, slot, index;
+	int rc;
+
+	for (pll_idx = 0; pll_idx < SIT9531X_NUM_PLLS; pll_idx++) {
+		rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+		if (rc)
+			return rc;
+
+		for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+			index = sit9531x_hw_src_input(srcs[slot]);
+			if (index >= sitdev->info->num_inputs)
+				continue;
+
+			/*
+			 * On a differentially configured pair only the P lane
+			 * has a DPLL pin, so that is the lane the connect and
+			 * disconnect callbacks account for.  Fold an N-lane
+			 * table entry onto its P lane, otherwise the count
+			 * would land on a lane nothing ever releases.  The
+			 * signalling mode is already known here:
+			 * sit9531x_input_mode_fetch() runs first.
+			 */
+			if (sit9531x_input_is_n(index) &&
+			    sitdev->ref[index].sig_mode == SIT9531X_MODE_DE)
+				index--;
+
+			sitdev->ref[index].pll_mask |= BIT(pll_idx);
+		}
+	}
+
+	return 0;
+}
+
+/*
+ * sit9531x_dev_state_fetch - read all hardware state at startup
+ *
+ * Called once during probe to populate the initial state cache.
+ */
+static int sit9531x_dev_state_fetch(struct sit9531x_dev *sitdev)
+{
+	int rc;
+	u8 i;
+
+	/* Detect SE/DE configuration before any per-lane access */
+	for (i = 0; i < sitdev->info->num_inputs / 2; i++) {
+		rc = sit9531x_input_mode_fetch(sitdev, i);
+		if (rc) {
+			dev_err(sitdev->dev,
+				"Failed to fetch CLKIN%u mode: %d\n", i, rc);
+			return rc;
+		}
+	}
+
+	for (i = 0; i < sitdev->info->num_inputs; i++) {
+		rc = sit9531x_ref_state_fetch(sitdev, i);
+		if (rc) {
+			dev_err(sitdev->dev,
+				"Failed to fetch input %u state: %d\n", i, rc);
+			return rc;
+		}
+	}
+
+	/*
+	 * The priority-table read walks the Page-1 registers, so it runs
+	 * with multiop_lock held like every other multi-register sequence.
+	 * Nothing can race with it here -- the DPLLs are not registered and
+	 * the monitor is not running yet -- but the page handling stays
+	 * serialized the same way as at runtime.
+	 */
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_ref_pll_mask_fetch(sitdev);
+	mutex_unlock(&sitdev->multiop_lock);
+	if (rc) {
+		dev_err(sitdev->dev,
+			"Failed to fetch input priority tables: %d\n", rc);
+		return rc;
+	}
+
+	for (i = 0; i < sitdev->info->num_outputs; i++) {
+		rc = sit9531x_out_state_fetch(sitdev, i);
+		if (rc) {
+			dev_err(sitdev->dev,
+				"Failed to fetch output %u state: %d\n", i, rc);
+			return rc;
+		}
+	}
+
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		rc = sit9531x_chan_state_fetch(sitdev, i);
+		if (rc) {
+			dev_err(sitdev->dev,
+				"Failed to fetch PLL%c state: %d\n",
+				'A' + i, rc);
+			return rc;
+		}
+	}
+
+	return 0;
+}
+
+static void sit9531x_dev_ref_states_update(struct sit9531x_dev *sitdev)
+{
+	int i, rc;
+
+	for (i = 0; i < sitdev->info->num_inputs; i++) {
+		rc = sit9531x_ref_state_fetch(sitdev, i);
+		if (rc)
+			dev_warn(sitdev->dev,
+				 "Failed to get REF%u status: %d\n", i, rc);
+	}
+}
+
+static void sit9531x_dev_chan_states_update(struct sit9531x_dev *sitdev)
+{
+	int i, rc;
+
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		rc = sit9531x_chan_state_fetch(sitdev, i);
+		if (rc)
+			dev_warn(sitdev->dev,
+				 "Failed to get PLL%c state: %d\n",
+				 'A' + i, rc);
+	}
+}
+
+/*
+ * sit9531x_dev_periodic_work - periodic hardware state polling
+ * @work:	kthread_work pointer
+ *
+ * Polls hardware state at SIT9531X_STATUS_POLL_MS intervals.
+ * Updates reference and channel states, then delegates change
+ * detection to sit9531x_dpll_changes_check() for each registered DPLL.
+ */
+static void sit9531x_dev_periodic_work(struct kthread_work *work)
+{
+	struct sit9531x_dev *sitdev = container_of(work, struct sit9531x_dev,
+						   work.work);
+	struct sit9531x_dpll *sitdpll;
+	int rc;
+
+	/*
+	 * Update the cached ref[]/chan[] arrays under multiop_lock so the
+	 * fetches are serialized against the DPLL callbacks that read
+	 * these fields and against the chip's page selector.
+	 *
+	 * The lock is then dropped before sit9531x_dpll_changes_check(),
+	 * which calls dpll_pin_change_ntf() / dpll_device_change_ntf().
+	 * Those notification helpers take DPLL-subsystem locks that are
+	 * already held when our callbacks are invoked from netlink
+	 * context, and nesting multiop_lock around them would invert the
+	 * lock order.  changes_check() reads the cache published above,
+	 * which is already consistent.
+	 */
+	mutex_lock(&sitdev->multiop_lock);
+	sit9531x_dev_ref_states_update(sitdev);
+	sit9531x_dev_chan_states_update(sitdev);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	list_for_each_entry(sitdpll, &sitdev->dplls, list)
+		sit9531x_dpll_changes_check(sitdpll);
+
+	/*
+	 * Acknowledge the chip's notification latches after the tick has
+	 * read and acted on them.  Without this, the W1C bits remain set
+	 * and -- on boards that wire INTRB -- the line stays asserted,
+	 * re-firing the threaded handler back to back.  The helper writes
+	 * W1C bits across page 0 and page 6 and must run under
+	 * multiop_lock to serialize the page selector against userspace
+	 * dpll ops.  Failure is non-fatal: status was already consumed
+	 * for this tick and the next tick re-processes whatever stayed
+	 * latched.
+	 */
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_clear_notifications(sitdev);
+	mutex_unlock(&sitdev->multiop_lock);
+	if (rc)
+		dev_warn_ratelimited(sitdev->dev,
+				     "Failed to clear notifications: %d\n",
+				     rc);
+
+	/* Run twice a second */
+	kthread_queue_delayed_work(sitdev->kworker, &sitdev->work,
+				   msecs_to_jiffies(SIT9531X_STATUS_POLL_MS));
+}
+
+/*
+ * sit9531x_irq_thread_fn - threaded IRQ handler for the chip's INTRB line
+ *
+ * Triggered when the chip asserts INTRB (and only when DT wires up the
+ * client interrupt; absent property == handler never installed).  The
+ * action mirrors a periodic-work tick: queue an immediate run so status
+ * registers are read and DPLL changes_check fires without waiting for
+ * the next poll deadline.  Polling continues to run as a fallback.
+ */
+static irqreturn_t sit9531x_irq_thread_fn(int irq, void *data)
+{
+	struct sit9531x_dev *sitdev = data;
+	int rc;
+
+	/*
+	 * Acknowledge the chip's notification latches from the threaded
+	 * handler itself.  With IRQF_ONESHOT the line is unmasked on
+	 * return, so deferring the W1C clear to the async kworker would
+	 * let a still-asserted INTRB re-fire immediately (interrupt storm).
+	 * Clear here, then kick the poll worker to read state and run
+	 * changes_check.
+	 */
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_clear_notifications(sitdev);
+	mutex_unlock(&sitdev->multiop_lock);
+	if (rc)
+		dev_warn_ratelimited(sitdev->dev,
+				     "IRQ: failed to clear notifications: %d\n",
+				     rc);
+
+	kthread_mod_delayed_work(sitdev->kworker, &sitdev->work, 0);
+	return IRQ_HANDLED;
+}
+
+/*
+ * sit9531x_dev_start - start normal operation
+ *
+ * Fetches initial hardware state, registers all DPLL devices and
+ * their pins, and starts the periodic monitoring thread.
+ */
+int sit9531x_dev_start(struct sit9531x_dev *sitdev)
+{
+	struct sit9531x_dpll *sitdpll;
+	int rc;
+
+	/* Fetch device state */
+	rc = sit9531x_dev_state_fetch(sitdev);
+	if (rc)
+		return rc;
+
+	list_for_each_entry(sitdpll, &sitdev->dplls, list) {
+		rc = sit9531x_dpll_register(sitdpll);
+		if (rc) {
+			dev_err_probe(sitdev->dev, rc,
+				      "Failed to register DPLL%u\n",
+				      sitdpll->id);
+			goto err_unregister;
+		}
+	}
+
+	kthread_queue_delayed_work(sitdev->kworker, &sitdev->work, 0);
+
+	return 0;
+
+err_unregister:
+	/*
+	 * Unregister what did register.  The caller frees the list on this
+	 * path, so leaving a DPLL registered would hand the subsystem a
+	 * pointer to freed memory.
+	 */
+	list_for_each_entry_continue_reverse(sitdpll, &sitdev->dplls, list)
+		sit9531x_dpll_unregister(sitdpll);
+
+	return rc;
+}
+
+/*
+ * sit9531x_dev_stop - stop normal operation
+ *
+ * Cancels the monitoring thread and unregisters all DPLL devices
+ * and their pins.
+ */
+void sit9531x_dev_stop(struct sit9531x_dev *sitdev)
+{
+	struct sit9531x_dpll *sitdpll;
+
+	kthread_cancel_delayed_work_sync(&sitdev->work);
+
+	list_for_each_entry(sitdpll, &sitdev->dplls, list) {
+		if (sitdpll->dpll_dev)
+			sit9531x_dpll_unregister(sitdpll);
+	}
+}
+
+static struct sit9531x_dpll_pin *
+sit9531x_dpll_pin_alloc(struct sit9531x_dpll *sitdpll,
+			enum dpll_pin_direction dir, u8 id)
+{
+	struct sit9531x_dpll_pin *pin;
+
+	pin = kzalloc_obj(*pin, GFP_KERNEL);
+	if (!pin)
+		return ERR_PTR(-ENOMEM);
+
+	pin->dpll = sitdpll;
+	pin->dir = dir;
+	pin->id = id;
+
+	return pin;
+}
+
+/*
+ * sit9531x_dpll_pin_register - register a DPLL pin with the subsystem
+ * @index:	absolute pin index for clock_id namespace
+ *
+ * Gets pin properties from firmware, creates or gets a dpll_pin,
+ * and registers it with the parent DPLL device.
+ */
+static int sit9531x_dpll_pin_register(struct sit9531x_dpll_pin *pin,
+				      u32 index)
+{
+	struct sit9531x_dpll *sitdpll = pin->dpll;
+	struct sit9531x_pin_props *props;
+	const struct dpll_pin_ops *ops;
+	int rc;
+
+	props = sit9531x_pin_props_get(sitdpll->dev, pin->dir, pin->id);
+	if (IS_ERR(props))
+		return PTR_ERR(props);
+
+	strscpy(pin->label, props->package_label, sizeof(pin->label));
+	pin->fwnode = fwnode_handle_get(props->fwnode);
+	pin->esync_control = props->esync_control;
+
+	pin->dpll_pin = dpll_pin_get(sitdpll->dev->clock_id, index,
+				     THIS_MODULE, &props->dpll_props,
+				     &pin->tracker);
+	if (IS_ERR(pin->dpll_pin)) {
+		rc = PTR_ERR(pin->dpll_pin);
+		goto err_pin_get;
+	}
+	dpll_pin_fwnode_set(pin->dpll_pin, props->fwnode);
+
+	ops = sit9531x_dpll_pin_ops_get(pin);
+
+	rc = dpll_pin_register(sitdpll->dpll_dev, pin->dpll_pin, ops, pin);
+	if (rc)
+		goto err_register;
+
+	sit9531x_pin_props_put(props);
+
+	return 0;
+
+err_register:
+	dpll_pin_put(pin->dpll_pin, &pin->tracker);
+err_pin_get:
+	/* dpll_pin_get() left an ERR_PTR here. */
+	pin->dpll_pin = NULL;
+	fwnode_handle_put(pin->fwnode);
+	pin->fwnode = NULL;
+	sit9531x_pin_props_put(props);
+
+	return rc;
+}
+
+static void sit9531x_dpll_pin_unregister(struct sit9531x_dpll_pin *pin)
+{
+	struct sit9531x_dpll *sitdpll = pin->dpll;
+	const struct dpll_pin_ops *ops;
+
+	ops = sit9531x_dpll_pin_ops_get(pin);
+
+	dpll_pin_unregister(sitdpll->dpll_dev, pin->dpll_pin, ops, pin);
+	dpll_pin_put(pin->dpll_pin, &pin->tracker);
+	pin->dpll_pin = NULL;
+
+	fwnode_handle_put(pin->fwnode);
+	pin->fwnode = NULL;
+}
+
+static void sit9531x_dpll_pins_unregister(struct sit9531x_dpll *sitdpll)
+{
+	struct sit9531x_dpll_pin *pin, *next;
+
+	list_for_each_entry_safe(pin, next, &sitdpll->pins, list) {
+		sit9531x_dpll_pin_unregister(pin);
+		list_del(&pin->list);
+		kfree(pin);
+	}
+}
+
+/*
+ * sit9531x_input_pin_is_registrable - check if an input pin is registrable
+ *
+ * Split out so input-model changes stay local to this helper.
+ *
+ * Return: true if the input pin should be registered, false otherwise
+ */
+static bool sit9531x_input_pin_is_registrable(struct sit9531x_dev *sitdev,
+					      u8 index)
+{
+	if (index >= sitdev->info->num_inputs)
+		return false;
+
+	/*
+	 * The N lane of a differentially-configured pair is not a
+	 * standalone input and is skipped (zl3073x model).
+	 */
+	if (sit9531x_input_is_n(index) &&
+	    sitdev->ref[index].sig_mode == SIT9531X_MODE_DE)
+		return false;
+
+	return true;
+}
+
+/*
+ * sit9531x_dpll_pin_is_registrable - check if a pin should be registered
+ * @dir:	pin direction
+ * @index:	pin hardware index
+ *
+ * For input pins: delegate to sit9531x_input_pin_is_registrable().
+ * For output pins: the pin is registrable if this DPLL is routed to it,
+ * whether or not it is currently driving.
+ *
+ * Return: true if pin should be registered, false otherwise
+ */
+static bool sit9531x_dpll_pin_is_registrable(struct sit9531x_dpll *sitdpll,
+					     enum dpll_pin_direction dir,
+					     u8 index)
+{
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+
+	if (dir == DPLL_PIN_DIRECTION_INPUT) {
+		/* The internal INTSYNC and XO pins are always registrable */
+		if (index == SIT9531X_INTSYNC_PIN_ID ||
+		    index == SIT9531X_MAX_INPUTS)
+			return true;
+
+		return sit9531x_input_pin_is_registrable(sitdev, index);
+	}
+
+	/* The internal INTSYNC source pin is always registrable */
+	if (index == SIT9531X_INTSYNC_OUT_PIN_ID)
+		return true;
+
+	/* Output -- check if driven by this DPLL */
+	if (index >= sitdev->info->num_outputs)
+		return false;
+
+	/*
+	 * Routing, not the current drive state: an output the initial
+	 * configuration muted still gets a pin, so userspace can see it and
+	 * un-mute it.  Only outputs no PLL drives are left unregistered.
+	 */
+	return sitdev->out[index].pll_idx == sitdpll->id &&
+	       sitdev->out[index].routed;
+}
+
+/*
+ * sit9531x_dpll_pins_register - register all registrable pins
+ *
+ * Enumerates all possible input and output pins, checks registrability,
+ * and registers each one.  Input pins come first, then output pins,
+ * with input pins first, then output pins.
+ */
+static int sit9531x_dpll_pins_register(struct sit9531x_dpll *sitdpll)
+{
+	struct sit9531x_dpll_pin *pin;
+	enum dpll_pin_direction dir;
+	u8 id, index;
+	int rc;
+
+	for (index = 0; index < SIT9531X_NUM_PINS_TOTAL; index++) {
+		if (index < SIT9531X_NUM_INPUT_PINS) {
+			id = index;
+			dir = DPLL_PIN_DIRECTION_INPUT;
+		} else {
+			id = index - SIT9531X_NUM_INPUT_PINS;
+			dir = DPLL_PIN_DIRECTION_OUTPUT;
+		}
+
+		if (!sit9531x_dpll_pin_is_registrable(sitdpll, dir, id))
+			continue;
+
+		pin = sit9531x_dpll_pin_alloc(sitdpll, dir, id);
+		if (IS_ERR(pin)) {
+			rc = PTR_ERR(pin);
+			goto error;
+		}
+
+		rc = sit9531x_dpll_pin_register(pin, index);
+		if (rc) {
+			kfree(pin);
+			goto error;
+		}
+
+		list_add(&pin->list, &sitdpll->pins);
+	}
+
+	return 0;
+
+error:
+	sit9531x_dpll_pins_unregister(sitdpll);
+	return rc;
+}
+
+static int sit9531x_dpll_device_register(struct sit9531x_dpll *sitdpll)
+{
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	int rc;
+
+	sitdpll->ops = sit9531x_dpll_device_ops;
+
+	sitdpll->dpll_dev = dpll_device_get(sitdev->clock_id, sitdpll->id,
+					    THIS_MODULE, &sitdpll->tracker);
+	if (IS_ERR(sitdpll->dpll_dev)) {
+		rc = PTR_ERR(sitdpll->dpll_dev);
+		sitdpll->dpll_dev = NULL;
+		return rc;
+	}
+
+	rc = dpll_device_register(sitdpll->dpll_dev,
+				  sit9531x_prop_dpll_type_get(sitdev,
+							      sitdpll->id),
+				  &sitdpll->ops, sitdpll);
+	if (rc) {
+		dpll_device_put(sitdpll->dpll_dev, &sitdpll->tracker);
+		sitdpll->dpll_dev = NULL;
+	}
+
+	return rc;
+}
+
+static void sit9531x_dpll_device_unregister(struct sit9531x_dpll *sitdpll)
+{
+	dpll_device_unregister(sitdpll->dpll_dev, &sitdpll->ops, sitdpll);
+	dpll_device_put(sitdpll->dpll_dev, &sitdpll->tracker);
+	sitdpll->dpll_dev = NULL;
+}
+
+/*
+ * sit9531x_dpll_alloc - allocate a DPLL device structure
+ * @sitdev:	parent device
+ * @ch:		PLL channel number (0-3)
+ *
+ * Return: pointer to allocated DPLL on success, error pointer on error
+ */
+struct sit9531x_dpll *sit9531x_dpll_alloc(struct sit9531x_dev *sitdev, u8 ch)
+{
+	struct sit9531x_dpll *sitdpll;
+
+	sitdpll = kzalloc_obj(*sitdpll, GFP_KERNEL);
+	if (!sitdpll)
+		return ERR_PTR(-ENOMEM);
+
+	sitdpll->dev = sitdev;
+	sitdpll->id = ch;
+	sitdpll->lock_status = DPLL_LOCK_STATUS_UNLOCKED;
+	INIT_LIST_HEAD(&sitdpll->pins);
+
+	return sitdpll;
+}
+
+/*
+ * sit9531x_dpll_free - deallocate a DPLL device structure
+ * @sitdpll:	DPLL to free
+ */
+void sit9531x_dpll_free(struct sit9531x_dpll *sitdpll)
+{
+	kfree(sitdpll);
+}
+
+/*
+ * sit9531x_dpll_register - register DPLL device and all its pins
+ *
+ * Registers the DPLL device with the subsystem and then registers
+ * all input and output pins that are connected to this PLL.
+ */
+int sit9531x_dpll_register(struct sit9531x_dpll *sitdpll)
+{
+	int rc;
+
+	rc = sit9531x_dpll_device_register(sitdpll);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_dpll_pins_register(sitdpll);
+	if (rc) {
+		sit9531x_dpll_device_unregister(sitdpll);
+		return rc;
+	}
+
+	return 0;
+}
+
+/* sit9531x_dpll_unregister - unregister DPLL device and its pins */
+void sit9531x_dpll_unregister(struct sit9531x_dpll *sitdpll)
+{
+	sit9531x_dpll_pins_unregister(sitdpll);
+	sit9531x_dpll_device_unregister(sitdpll);
+}
+
+static void sit9531x_dpll_list_free(struct sit9531x_dev *sitdev)
+{
+	struct sit9531x_dpll *sitdpll, *next;
+
+	list_for_each_entry_safe(sitdpll, next, &sitdev->dplls, list) {
+		list_del(&sitdpll->list);
+		sit9531x_dpll_free(sitdpll);
+	}
+}
+
+/* Runs only once the device is fully started, see the caller. */
+static void sit9531x_dev_dpll_fini(void *ptr)
+{
+	struct sit9531x_dev *sitdev = ptr;
+
+	sit9531x_dev_stop(sitdev);
+	kthread_destroy_worker(sitdev->kworker);
+	sit9531x_dpll_list_free(sitdev);
+}
+
+static int sit9531x_devm_dpll_init(struct sit9531x_dev *sitdev)
+{
+	struct kthread_worker *kworker;
+	struct sit9531x_dpll *sitdpll;
+	unsigned int i;
+	int rc;
+
+	INIT_LIST_HEAD(&sitdev->dplls);
+	kthread_init_delayed_work(&sitdev->work, sit9531x_dev_periodic_work);
+
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		sitdpll = sit9531x_dpll_alloc(sitdev, i);
+		if (IS_ERR(sitdpll)) {
+			rc = dev_err_probe(sitdev->dev, PTR_ERR(sitdpll),
+					   "Failed to alloc DPLL%u\n", i);
+			goto err_free_dplls;
+		}
+
+		list_add_tail(&sitdpll->list, &sitdev->dplls);
+	}
+
+	kworker = kthread_run_worker(0, "sit9531x-%s", dev_name(sitdev->dev));
+	if (IS_ERR(kworker)) {
+		rc = PTR_ERR(kworker);
+		goto err_free_dplls;
+	}
+	sitdev->kworker = kworker;
+
+	rc = sit9531x_dev_start(sitdev);
+	if (rc) {
+		rc = dev_err_probe(sitdev->dev, rc, "Failed to start device\n");
+		goto err_destroy_worker;
+	}
+
+	/*
+	 * Only now is every field the cleanup touches valid, so this is the
+	 * first point at which the action may be registered.  On failure it
+	 * runs the action itself, which is correct here and only here.
+	 */
+	return devm_add_action_or_reset(sitdev->dev, sit9531x_dev_dpll_fini,
+					sitdev);
+
+err_destroy_worker:
+	kthread_destroy_worker(sitdev->kworker);
+err_free_dplls:
+	sit9531x_dpll_list_free(sitdev);
+
+	return rc;
+}
+
+/*
+ * sit9531x_read_variant_id - read chip variant ID byte from hardware
+ * @id:		output variant ID byte
+ *
+ * Reads the single-byte variant identification register from Page 0
+ * reg 0x02 (95317 = 0x17, 95316 = 0x31).  Reg 0x03 holds a separate
+ * revision byte and is intentionally not consumed here.
+ */
+static int sit9531x_read_variant_id(struct sit9531x_dev *sitdev, u8 *id)
+{
+	return sit9531x_read_u8(sitdev, SIT9531X_REG_VARIANT_ID, id);
+}
+
+static const struct sit9531x_chip_info *sit9531x_match_variant(u8 id)
+{
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(sit9531x_chip_ids); i++) {
+		if (sit9531x_chip_ids[i].id == id)
+			return &sit9531x_chip_ids[i];
+	}
+
+	return NULL;
+}
+
+/*
+ * sit9531x_derive_clock_id - build EUI-64 clock identifier
+ *
+ * Generates a deterministic 64-bit identifier from the SiTime OUI,
+ * the chip ID, and the I2C address.  This provides a stable clock_id
+ * across reboots.
+ *
+ * Return: 64-bit clock identifier
+ */
+static u64 sit9531x_derive_clock_id(struct sit9531x_dev *sitdev)
+{
+	u64 clkid;
+
+	clkid  = SIT9531X_OUI << 24;
+	clkid |= (u64)sitdev->info->id << 8;
+	clkid |= (u64)sitdev->client->addr;
+
+	return clkid;
+}
+
+/*
+ * Board-config overrides for fixed efuse/blob routing the chip registers do
+ * not describe unambiguously.  Absent properties leave pll_fvco[] zeroed
+ * (derive from DIVN) and out_pll_map_valid false (use the OUT_MAP registers).
+ */
+static void sit9531x_parse_board_config(struct sit9531x_dev *sitdev)
+{
+	u32 map[SIT9531X_MAX_OUTPUTS];
+	int n, i;
+
+	device_property_read_u64_array(sitdev->dev, "sitime,pll-fvco",
+				       sitdev->pll_fvco, SIT9531X_NUM_PLLS);
+
+	if (!device_property_present(sitdev->dev, "sitime,output-pll-map"))
+		return;
+
+	/*
+	 * Any 1..MAX_OUTPUTS length is accepted so the 8-output SiT95317 need
+	 * not pad to 12; variant detection has not run yet and entries past
+	 * the detected num_outputs are never indexed.  Trailing entries of a
+	 * short map must read as unmapped rather than 0 (== PLLA), which
+	 * would mark unrouted outputs active in sit9531x_out_state_fetch().
+	 */
+	memset(sitdev->out_pll_map, SIT9531X_OUT_PLL_UNMAPPED,
+	       sizeof(sitdev->out_pll_map));
+
+	n = device_property_count_u32(sitdev->dev, "sitime,output-pll-map");
+	if (n <= 0 || n > SIT9531X_MAX_OUTPUTS ||
+	    device_property_read_u32_array(sitdev->dev, "sitime,output-pll-map",
+					   map, n))
+		return;
+
+	for (i = 0; i < n; i++)
+		sitdev->out_pll_map[i] = map[i];
+	sitdev->out_pll_map_valid = true;
+}
+
+int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
+{
+	struct clk *xtal_clk;
+	u8 variant_id;
+	int rc;
+
+	/*
+	 * Fvco = Fref * (DIVN + frac/2^32) with Fref = xtal_freq << doubler,
+	 * so every freq_set and phase_adjust path divides by a rate derived
+	 * from the XO feeding XIN/XO_CLK.
+	 */
+	xtal_clk = devm_clk_get_enabled(sitdev->dev, "xtal");
+	if (IS_ERR(xtal_clk))
+		return dev_err_probe(sitdev->dev, PTR_ERR(xtal_clk),
+				     "Failed to get xtal clock\n");
+	sitdev->xtal_freq = clk_get_rate(xtal_clk);
+	if (!sitdev->xtal_freq)
+		return dev_err_probe(sitdev->dev, -EINVAL,
+				     "xtal clock has no rate\n");
+
+	/*
+	 * Held deasserted, never pulsed: the chip configuration comes from
+	 * efuse or an NVM blob applied before probe, and a reset would
+	 * discard it.  Must precede the first I2C access, as a board that
+	 * powers up asserted keeps the chip unreachable until released.
+	 */
+	sitdev->reset_gpio = devm_gpiod_get_optional(sitdev->dev, "reset",
+						     GPIOD_OUT_LOW);
+	if (IS_ERR(sitdev->reset_gpio))
+		return dev_err_probe(sitdev->dev, PTR_ERR(sitdev->reset_gpio),
+				     "Failed to request reset gpio\n");
+	if (sitdev->reset_gpio)
+		fsleep(10000);	/* internal boot after release */
+
+	sit9531x_parse_board_config(sitdev);
+
+	rc = sit9531x_read_variant_id(sitdev, &variant_id);
+	if (rc)
+		return rc;
+
+	sitdev->info = sit9531x_match_variant(variant_id);
+	if (!sitdev->info)
+		return dev_err_probe(sitdev->dev, -ENODEV,
+				     "Unknown variant ID: 0x%02x\n", variant_id);
+
+	sitdev->clock_id = sit9531x_derive_clock_id(sitdev);
+	sitdev->intsync_src = -1;
+
+	rc = devm_mutex_init(sitdev->dev, &sitdev->multiop_lock);
+	if (rc)
+		return dev_err_probe(sitdev->dev, rc,
+				     "Failed to initialize mutex\n");
+
+	/*
+	 * Before the IRQ: the handler reaches sitdev->kworker through
+	 * kthread_mod_delayed_work(), so the worker has to exist before an
+	 * INTRB assertion can land.
+	 */
+	rc = sit9531x_devm_dpll_init(sitdev);
+	if (rc)
+		return rc;
+
+	/* Absent "interrupts" leaves client->irq 0 and the poll in charge. */
+	sitdev->irq = sitdev->client ? sitdev->client->irq : 0;
+	if (sitdev->irq > 0) {
+		rc = devm_request_threaded_irq(sitdev->dev, sitdev->irq,
+					       NULL, sit9531x_irq_thread_fn,
+					       IRQF_ONESHOT,
+					       dev_name(sitdev->dev), sitdev);
+		if (rc)
+			return dev_err_probe(sitdev->dev, rc,
+					     "Failed to request IRQ %d\n",
+					     sitdev->irq);
+	}
+
+	return 0;
+}
+
+static int sit9531x_i2c_probe(struct i2c_client *client)
+{
+	struct sit9531x_dev *sitdev;
+	struct regmap *regmap;
+
+	regmap = devm_regmap_init_i2c(client, &sit9531x_regmap_config);
+	if (IS_ERR(regmap))
+		return dev_err_probe(&client->dev, PTR_ERR(regmap),
+				     "Failed to initialize regmap\n");
+
+	sitdev = devm_kzalloc(&client->dev, sizeof(*sitdev), GFP_KERNEL);
+	if (!sitdev)
+		return -ENOMEM;
+
+	sitdev->dev = &client->dev;
+	sitdev->client = client;
+	sitdev->regmap = regmap;
+	i2c_set_clientdata(client, sitdev);
+
+	return sit9531x_dev_probe(sitdev);
+}
+
+static const struct of_device_id sit9531x_of_match[] = {
+	{ .compatible = "sitime,sit95316" },
+	{ .compatible = "sitime,sit95317" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, sit9531x_of_match);
+
+static struct i2c_driver sit9531x_i2c_driver = {
+	.driver = {
+		.name		= "sit9531x",
+		.of_match_table	= sit9531x_of_match,
+	},
+	.probe		= sit9531x_i2c_probe,
+};
+module_i2c_driver(sit9531x_i2c_driver);
+
+MODULE_AUTHOR("Ali Rouhi <arouhi@sitime.com>");
+MODULE_AUTHOR("Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>");
+MODULE_DESCRIPTION("SiTime SiT9531x DPLL subsystem driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
new file mode 100644
index 000000000000..05d4ee0c712e
--- /dev/null
+++ b/drivers/dpll/sit9531x/core.h
@@ -0,0 +1,372 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * SiTime SiT9531x DPLL core driver
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * Device structure, register access helpers, and core function
+ * declarations.
+ */
+
+#ifndef _SIT9531X_CORE_H
+#define _SIT9531X_CORE_H
+
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/kthread.h>
+#include <linux/list.h>
+#include <linux/mutex.h>
+#include <linux/regmap.h>
+#include <linux/types.h>
+
+#include "regs.h"
+
+#define SIT9531X_NUM_PLLS		4
+#define SIT9531X_MAX_INPUTS		8
+#define SIT9531X_NUM_INPUT_PAIRS	(SIT9531X_MAX_INPUTS / 2)
+#define SIT9531X_MAX_OUTPUTS		12
+/* out_pll_map[] entry meaning "this output is not routed to any PLL" */
+#define SIT9531X_OUT_PLL_UNMAPPED	0xFF
+/*
+ * INTSYNC (the inter-PLL sync net) is modelled as two pins.  The
+ * destination PLL that locks to INTSYNC sees an input pin
+ * (SIT9531X_INTSYNC_PIN_ID, in the input id namespace after the physical
+ * inputs and the xtal); the source PLL that drives INTSYNC sees an output
+ * pin (SIT9531X_INTSYNC_OUT_PIN_ID, appended after the physical outputs).
+ */
+#define SIT9531X_INTSYNC_PIN_ID		(SIT9531X_MAX_INPUTS + 1)
+#define SIT9531X_INTSYNC_OUT_PIN_ID	SIT9531X_MAX_OUTPUTS
+#define SIT9531X_NUM_PINS		(SIT9531X_MAX_INPUTS + 2 + SIT9531X_MAX_OUTPUTS + 1)
+#define SIT9531X_STATUS_POLL_MS		500
+
+/* selected_ref value when the active source is not a registered input */
+#define SIT9531X_REF_INVALID		0xFF
+
+/* SiTime IEEE OUI for EUI-64 generation */
+#define SIT9531X_OUI			0x0090C2FFFEULL
+
+struct sit9531x_dpll;
+
+/*
+ * struct sit9531x_chip_info - chip variant identification
+ * @id:		variant ID byte read from register
+ * @num_inputs:	number of input clock pins
+ * @num_outputs: number of output clock pins
+ * @name:	human-readable variant name
+ * @clkout_map:	per-output slot mapping (output index -> physical slot)
+ */
+struct sit9531x_chip_info {
+	u8		id;
+	u8		num_inputs;
+	u8		num_outputs;
+	const char	*name;
+	const u8	*clkout_map;
+};
+
+/*
+ * enum sit9531x_signal_mode - input signal electrical mode
+ * @SIT9531X_MODE_SE: single-ended
+ * @SIT9531X_MODE_DE: differential
+ */
+enum sit9531x_signal_mode {
+	SIT9531X_MODE_SE = 0,
+	SIT9531X_MODE_DE,
+};
+
+/*
+ * struct sit9531x_ref - input reference state
+ * @freq:		configured frequency in Hz
+ * @enabled:		reference is enabled for monitoring
+ * @los:		loss-of-signal detected
+ * @oof:		out-of-frequency detected
+ * @pll_mask:		bitmask of PLLs this input feeds (bit 0 = PLLA)
+ * @label:		board label from DT or default
+ * @sig_mode:		signal mode of the pair this lane belongs to
+ *			(detected from CLKINx_INPUT_MODE at probe)
+ */
+struct sit9531x_ref {
+	u32		freq;
+	bool		enabled;
+	bool		los;
+	bool		oof;
+	u8		pll_mask;
+	const char	*label;
+	enum sit9531x_signal_mode	sig_mode;
+};
+
+/*
+ * struct sit9531x_out - output state
+ * @enabled:		output is driving, i.e. not forced into Hi-Z
+ * @routed:		output is mapped to @pll_idx by the initial
+ *			configuration; an unrouted output has no DPLL pin
+ * @pll_idx:		PLL driving this output (0-3)
+ * @label:		board label from DT or default
+ */
+struct sit9531x_out {
+	u32		freq;
+	bool		enabled;
+	bool		routed;
+	u8		pll_idx;
+	const char	*label;
+};
+
+/*
+ * struct sit9531x_chan - per-PLL channel state
+ * @active:		PLL has reached its active state; a PLL the loaded
+ *			configuration leaves unused never does, and its
+ *			loss-of-lock bit stays clear because nothing drives it
+ * @locked:		PLL is locked (raw status register bit)
+ * @mode:		0 = sync (outer loop enabled), 1 = free-run
+ * @selected_ref:	logical input index of the currently selected
+ *			reference (the INTSYNC net maps to
+ *			SIT9531X_INTSYNC_PIN_ID), or SIT9531X_REF_INVALID
+ *			when the hardware source encoding is reserved
+ * @inner_lol:		PLL inner loop loss-of-lock detected
+ * @ho_freeze:		holdover freeze active
+ */
+struct sit9531x_chan {
+	bool		active;
+	bool		locked;
+	u8		mode;
+	u8		selected_ref;
+	bool		inner_lol;
+	bool		ho_freeze;
+};
+
+/*
+ * struct sit9531x_dev - SiT9531x device instance
+ * @info:		detected chip variant info
+ * @multiop_lock:	mutex for multi-register atomic operations
+ * @ref:		array of input reference states
+ * @out:		array of output states
+ * @chan:		array of per-PLL channel states
+ * @xtal_freq:		crystal oscillator frequency in Hz
+ * @kworker:		kthread worker for periodic polling
+ * @work:		delayed work for periodic state checks
+ * @clock_id:		IEEE 1588 EUI-64 clock identifier
+ * @reset_gpio:		optional reset line (DT "reset-gpios"), NULL if absent
+ * @irq:		optional INTRB IRQ number (from DT "interrupts" via the
+ *			I2C client), 0 if no IRQ is wired
+ * @pll_fvco:		optional per-PLL VCO in Hz from DT
+ *			"sitime,pll-fvco"; 0 means derive from DIVN
+ * @out_pll_map:	optional per-output source PLL (0-3, 0xff =
+ *			unmapped) from DT "sitime,output-pll-map"
+ * @out_pll_map_valid:	true when out_pll_map[] was populated from DT;
+ *			false means use the chip's OUT_MAP registers
+ * @intsync_src:	PLL index currently sourcing inter-PLL
+ *			synchronization (INTSYNC), or -1 when disabled
+ */
+struct sit9531x_dev {
+	struct device			*dev;
+	struct i2c_client		*client;
+	struct regmap			*regmap;
+	const struct sit9531x_chip_info	*info;
+	/* Serializes multi-step register sequences */
+	struct mutex			multiop_lock;
+
+	/* Hardware state */
+	struct sit9531x_ref	ref[SIT9531X_MAX_INPUTS + 1]; /* +1 for xtal */
+	struct sit9531x_out	out[SIT9531X_MAX_OUTPUTS];
+	struct sit9531x_chan	chan[SIT9531X_NUM_PLLS];
+	u32			xtal_freq;
+
+	/* DPLL channels */
+	struct list_head	dplls;
+
+	/* Monitor */
+	struct kthread_worker		*kworker;
+	struct kthread_delayed_work	work;
+
+	/* Device identity */
+	u64			clock_id;
+
+	/* Optional DT-described GPIO / IRQ lines */
+	struct gpio_desc	*reset_gpio;
+	int			irq;
+
+	/* Optional DT board-config overrides */
+	u64			pll_fvco[SIT9531X_NUM_PLLS];
+	u8			out_pll_map[SIT9531X_MAX_OUTPUTS];
+	bool			out_pll_map_valid;
+
+	/* Inter-PLL synchronization state */
+	s8			intsync_src;
+
+};
+
+extern const struct regmap_config sit9531x_regmap_config;
+
+/* ---- Core lifecycle ---- */
+int  sit9531x_dev_probe(struct sit9531x_dev *sitdev);
+int  sit9531x_dev_start(struct sit9531x_dev *sitdev);
+void sit9531x_dev_stop(struct sit9531x_dev *sitdev);
+
+/* ---- Register access ---- */
+int sit9531x_read_u8(struct sit9531x_dev *sitdev, unsigned int reg,
+		     u8 *val);
+int sit9531x_write_u8(struct sit9531x_dev *sitdev, unsigned int reg,
+		      u8 val);
+int sit9531x_read_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+			 u8 offset, u8 *val);
+int sit9531x_write_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+			  u8 offset, u8 val);
+int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+			   u8 offset, u8 mask, u8 val);
+
+/* ---- Input enable/disable ---- */
+int sit9531x_input_disable(struct sit9531x_dev *sitdev, u8 index);
+int sit9531x_input_enable(struct sit9531x_dev *sitdev, u8 index);
+
+/* ---- Input priority ---- */
+int sit9531x_input_prio_set(struct sit9531x_dev *sitdev, u8 pll_idx,
+			    u8 input_idx, u8 prio);
+int sit9531x_input_prio_get(struct sit9531x_dev *sitdev, u8 pll_idx,
+			    u8 input_idx, u8 *prio);
+int sit9531x_input_prio_remove(struct sit9531x_dev *sitdev, u8 pll_idx,
+			       u8 input_idx);
+int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
+			    u8 input_idx);
+
+/* ---- Output enable/disable (Hi-Z control) ---- */
+int sit9531x_output_disable(struct sit9531x_dev *sitdev, u8 index);
+int sit9531x_output_enable(struct sit9531x_dev *sitdev, u8 index);
+
+/* ---- Output frequency ---- */
+int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
+			     u8 pll_idx, u64 frequency);
+int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
+			     u64 *frequency);
+
+/* ---- Output phase adjust (PRG_RST_DELAY register-based) ---- */
+int sit9531x_output_phase_adjust_set(struct sit9531x_dev *sitdev,
+				     u8 out_idx, s32 phase_ps);
+
+/* ---- Notification clear ---- */
+int sit9531x_clear_notifications(struct sit9531x_dev *sitdev);
+
+/* ---- INTSYNC (inter-PLL synchronization) ---- */
+int sit9531x_intsync_enable(struct sit9531x_dev *sitdev, u8 src_pll_idx);
+int sit9531x_intsync_disable(struct sit9531x_dev *sitdev, u8 src_pll_idx);
+
+/* ---- Output pulse control ---- */
+int sit9531x_output_pulse_ctrl_set(struct sit9531x_dev *sitdev,
+				   u8 out_idx, u8 pulse_ctrl);
+
+/* ---- Phase offset (TDC readback) ---- */
+int sit9531x_pll_ffo_ppt(struct sit9531x_dev *sitdev, u8 pll_idx, s64 *ffo);
+int sit9531x_phase_offset_read(struct sit9531x_dev *sitdev, u8 pll_idx,
+			       s64 *phase_ps);
+
+/* ---- State helpers ---- */
+
+/*
+ * sit9531x_pll_page - get register page for PLL index
+ * @pll_idx: PLL index (0 = PLLA, 3 = PLLD)
+ */
+static inline u8 sit9531x_pll_page(u8 pll_idx)
+{
+	return SIT9531X_PAGE_PLLA + pll_idx;
+}
+
+/*
+ * Logical input pins are interleaved: even index = P lane, odd
+ * index = N lane of pair index/2 (IN0P, IN0N, IN1P, IN1N, ...).
+ * Index SIT9531X_MAX_INPUTS is the XO input.
+ */
+
+/*
+ * sit9531x_input_pair - get input pair number for a logical input index
+ * @index: logical input pin index
+ */
+static inline u8 sit9531x_input_pair(u8 index)
+{
+	return index >> 1;
+}
+
+/*
+ * sit9531x_input_is_n - check if a logical input index is an N lane
+ * @index: logical input pin index
+ */
+static inline bool sit9531x_input_is_n(u8 index)
+{
+	return index & 1;
+}
+
+/*
+ * sit9531x_input_hw_src - translate logical input index to source encoding
+ * @index: logical input pin index
+ *
+ * The priority table and CLK_ACTIVESEL registers use a non-contiguous
+ * source encoding: 0-3 = CLK0P..CLK3P, 5 = OCXO, 6 = INTSYNC,
+ * 7-10 = CLK0N..CLK3N.
+ */
+static inline u8 sit9531x_input_hw_src(u8 index)
+{
+	if (index == SIT9531X_MAX_INPUTS)
+		return SIT9531X_PRIO_SRC_OCXO;
+	if (index == SIT9531X_INTSYNC_PIN_ID)
+		return SIT9531X_PRIO_SRC_INTSYNC;
+	if (sit9531x_input_is_n(index))
+		return SIT9531X_PRIO_SRC_N_BASE + sit9531x_input_pair(index);
+	return sit9531x_input_pair(index);
+}
+
+/*
+ * sit9531x_hw_src_input - translate source encoding to logical input index
+ * @src: 4-bit hardware source encoding
+ *
+ * Return: logical input index (INTSYNC maps to SIT9531X_INTSYNC_PIN_ID),
+ * or SIT9531X_REF_INVALID if @src is a reserved value
+ */
+static inline u8 sit9531x_hw_src_input(u8 src)
+{
+	if (src < SIT9531X_NUM_INPUT_PAIRS)
+		return src * 2;
+	if (src == SIT9531X_PRIO_SRC_OCXO)
+		return SIT9531X_MAX_INPUTS;
+	if (src == SIT9531X_PRIO_SRC_INTSYNC)
+		return SIT9531X_INTSYNC_PIN_ID;
+	if (src >= SIT9531X_PRIO_SRC_N_BASE &&
+	    src < SIT9531X_PRIO_SRC_N_BASE + SIT9531X_NUM_INPUT_PAIRS)
+		return (src - SIT9531X_PRIO_SRC_N_BASE) * 2 + 1;
+	return SIT9531X_REF_INVALID;
+}
+
+/*
+ * sit9531x_ref_state_get - get reference state by index
+ * @index:	logical input index
+ *
+ * Return: pointer to the cached input reference state
+ */
+static inline const struct sit9531x_ref *
+sit9531x_ref_state_get(const struct sit9531x_dev *sitdev, u8 index)
+{
+	return &sitdev->ref[index];
+}
+
+/*
+ * sit9531x_out_state_get - get output state by index
+ * @index:	logical output index
+ *
+ * Return: pointer to the cached output state
+ */
+static inline const struct sit9531x_out *
+sit9531x_out_state_get(const struct sit9531x_dev *sitdev, u8 index)
+{
+	return &sitdev->out[index];
+}
+
+/*
+ * sit9531x_chan_state_get - get channel state by PLL index
+ *
+ * Return: pointer to the cached per-PLL channel state
+ */
+static inline const struct sit9531x_chan *
+sit9531x_chan_state_get(const struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+	return &sitdev->chan[pll_idx];
+}
+
+#endif /* _SIT9531X_CORE_H */
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
new file mode 100644
index 000000000000..e258beceffdd
--- /dev/null
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -0,0 +1,1232 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * SiTime SiT9531x DPLL subsystem callbacks and registration
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * DPLL device ops, pin ops (separate input/output), pin registration,
+ * and periodic change detection.
+ */
+
+#include <linux/dpll.h>
+#include <linux/err.h>
+#include <linux/kthread.h>
+#include <linux/list.h>
+#include <linux/netlink.h>
+#include <linux/slab.h>
+
+#include "core.h"
+#include "dpll.h"
+#include "prop.h"
+#include "regs.h"
+
+#define SIT9531X_ESYNC_FREQ_10MHZ	10000000ULL
+#define SIT9531X_ESYNC_PULSE_DEFAULT	50
+
+static const struct dpll_pin_frequency sit9531x_esync_ranges[] = {
+	DPLL_PIN_FREQUENCY(0),
+	DPLL_PIN_FREQUENCY(SIT9531X_ESYNC_FREQ_10MHZ),
+};
+
+static inline bool
+sit9531x_dpll_esync_pin_supported(const struct sit9531x_dpll_pin *dpin)
+{
+	return dpin->esync_control;
+}
+
+static inline bool sit9531x_dpll_is_input_pin(const struct sit9531x_dpll_pin *pin)
+{
+	return pin->dir == DPLL_PIN_DIRECTION_INPUT;
+}
+
+static inline bool
+sit9531x_dpll_is_intsync_pin(const struct sit9531x_dpll_pin *pin)
+{
+	return sit9531x_dpll_is_input_pin(pin) &&
+	       pin->id == SIT9531X_INTSYNC_PIN_ID;
+}
+
+static inline bool
+sit9531x_dpll_is_intsync_src_pin(const struct sit9531x_dpll_pin *pin)
+{
+	return !sit9531x_dpll_is_input_pin(pin) &&
+	       pin->id == SIT9531X_INTSYNC_OUT_PIN_ID;
+}
+
+static inline bool
+sit9531x_dpll_is_xo_pin(const struct sit9531x_dpll_pin *pin)
+{
+	return sit9531x_dpll_is_input_pin(pin) &&
+	       pin->id == SIT9531X_MAX_INPUTS;
+}
+
+/*
+ * The cached state this reports comes from the outer loss-of-lock byte
+ * (page 0, reg 0x06), the PLL mode bit (PLL page, reg 0x31), inner LOL
+ * (reg 0x92) and the holdover freeze byte (reg 0x0A).
+ */
+static int
+sit9531x_dpll_lock_status_get(const struct dpll_device *dpll, void *dpll_priv,
+			      enum dpll_lock_status *status,
+			      enum dpll_lock_status_error *status_error,
+			      struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	const struct sit9531x_chan *chan;
+
+	if (status_error)
+		*status_error = DPLL_LOCK_STATUS_ERROR_NONE;
+
+	chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	if (!chan->active) {
+		/*
+		 * A PLL the loaded configuration leaves unused never reaches
+		 * its active state.  Nothing drives its loss-of-lock bit, so
+		 * without this it would report a lock it does not have.
+		 */
+		*status = DPLL_LOCK_STATUS_UNLOCKED;
+	} else if (chan->locked) {
+		if (chan->mode)
+			*status = DPLL_LOCK_STATUS_LOCKED;       /* free-run */
+		else
+			*status = DPLL_LOCK_STATUS_LOCKED_HO_ACQ; /* sync */
+	} else if (chan->ho_freeze) {
+		*status = DPLL_LOCK_STATUS_HOLDOVER;
+	} else {
+		*status = DPLL_LOCK_STATUS_UNLOCKED;
+	}
+
+	/* Report inner LOL as an error condition */
+	if (status_error && chan->inner_lol)
+		*status_error = DPLL_LOCK_STATUS_ERROR_UNDEFINED;
+
+	mutex_unlock(&sitdev->multiop_lock);
+
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_mode_get - report current PLL operating mode
+ *
+ * reads outer loop disable bit (PLL page reg 0x31[5]).
+ * Free-run -> MANUAL, sync -> AUTOMATIC.
+ */
+static int
+sit9531x_dpll_mode_get(const struct dpll_device *dpll, void *dpll_priv,
+		       enum dpll_mode *mode, struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	const struct sit9531x_chan *chan;
+
+	chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
+
+	mutex_lock(&sitdev->multiop_lock);
+	*mode = chan->mode ? DPLL_MODE_MANUAL : DPLL_MODE_AUTOMATIC;
+	mutex_unlock(&sitdev->multiop_lock);
+
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_mode_set - switch PLL between free-run and sync mode
+ *
+ * writes PLL page reg 0x31[5] to enable/disable the
+ * outer loop, then triggers a small update via reg 0x0F.
+ *
+ * The driver maps AUTOMATIC to sync mode (outer loop enabled; the PLL
+ * selects its reference automatically from the priority table) and
+ * MANUAL to free-run (outer loop disabled via bit 0x31[5]). The chip
+ * also has a separate manual-active-select path (MANUAL_ACTIVE_SEL_PLL
+ * plus the per-PLL man_in_sel registers) that can pin one reference
+ * while still syncing; that path is not wired to a DPLL callback here.
+ */
+static int
+sit9531x_dpll_mode_set(const struct dpll_device *dpll, void *dpll_priv,
+		       enum dpll_mode mode, struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	u8 val;
+	int rc;
+
+	/*
+	 * Outer loop disable bit:
+	 *   0 = sync mode (outer loop enabled)  -> AUTOMATIC
+	 *   1 = free-run (outer loop disabled)  -> MANUAL
+	 */
+	val = (mode == DPLL_MODE_MANUAL) ? SIT9531X_PLL_STATUS_OUTER_DIS : 0;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	rc = sit9531x_update_pll_u8(sitdev, sitdpll->id,
+				    SIT9531X_PLL_REG_STATUS,
+				    SIT9531X_PLL_STATUS_OUTER_DIS, val);
+	if (rc) {
+		NL_SET_ERR_MSG(extack, "Failed to write PLL mode register");
+		goto unlock;
+	}
+
+	/* Trigger small update to apply without full NVM cycle */
+	rc = sit9531x_write_pll_u8(sitdev, sitdpll->id,
+				   SIT9531X_PLL_REG_SMALL_UPDATE,
+				   SIT9531X_SMALL_UPDATE_CMD);
+	if (rc) {
+		NL_SET_ERR_MSG(extack, "Failed to trigger small update");
+		goto unlock;
+	}
+
+	/*
+	 * Keep the cached mode in step with the register.  The periodic
+	 * monitor refreshes it too, but mode_get and the reference-selection
+	 * path read this cache and would otherwise report or act on the
+	 * pre-switch mode until the next poll.
+	 */
+	sitdev->chan[sitdpll->id].mode = !!val;
+
+unlock:
+	mutex_unlock(&sitdev->multiop_lock);
+
+	return rc;
+}
+
+static int
+sit9531x_dpll_supported_modes_get(const struct dpll_device *dpll,
+				  void *dpll_priv, unsigned long *modes,
+				  struct netlink_ext_ack *extack)
+{
+	__set_bit(DPLL_MODE_AUTOMATIC, modes);
+	__set_bit(DPLL_MODE_MANUAL, modes);
+
+	return 0;
+}
+
+const struct dpll_device_ops sit9531x_dpll_device_ops = {
+	.lock_status_get	= sit9531x_dpll_lock_status_get,
+	.mode_get		= sit9531x_dpll_mode_get,
+	.mode_set		= sit9531x_dpll_mode_set,
+	.supported_modes_get	= sit9531x_dpll_supported_modes_get,
+	/* temp_get not available -- SiT9531x has no on-die temp sensor */
+};
+
+static int
+sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
+				      void *pin_priv,
+				      const struct dpll_device *dpll,
+				      void *dpll_priv,
+				      enum dpll_pin_direction *direction,
+				      struct netlink_ext_ack *extack)
+{
+	*direction = DPLL_PIN_DIRECTION_INPUT;
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_input_pin_frequency_get - read input pin frequency
+ *
+ * returns cached frequency from DT or last set.
+ */
+static int
+sit9531x_dpll_input_pin_frequency_get(const struct dpll_pin *pin,
+				      void *pin_priv,
+				      const struct dpll_device *dpll,
+				      void *dpll_priv, u64 *frequency,
+				      struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	const struct sit9531x_ref *ref;
+
+	ref = sit9531x_ref_state_get(sitdpll->dev, dpin->id);
+	*frequency = ref->freq;
+
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_input_pin_state_on_dpll_get - get input pin DPLL state
+ *
+ * determines pin state from channel state: connected
+ * if this input is the selected reference on a locked PLL, selectable
+ * if enabled in automatic mode, disconnected otherwise.
+ */
+static int
+sit9531x_dpll_input_pin_state_on_dpll_get(const struct dpll_pin *pin,
+					  void *pin_priv,
+					  const struct dpll_device *dpll,
+					  void *dpll_priv,
+					  enum dpll_pin_state *state,
+					  struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	const struct sit9531x_chan *chan;
+	const struct sit9531x_ref *ref;
+
+	chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
+	ref = sit9531x_ref_state_get(sitdev, dpin->id);
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	/*
+	 * Report CONNECTED only when the PLL is genuinely locked to this
+	 * input.  chan->locked already excludes a PLL the configuration
+	 * leaves unused, which would otherwise claim its default source as
+	 * connected right after probe; the inner loop has to be locked as
+	 * well before the reference can be called connected.
+	 */
+	if (chan->locked && !chan->inner_lol &&
+	    chan->selected_ref == dpin->id)
+		*state = DPLL_PIN_STATE_CONNECTED;
+	/*
+	 * Selectable means this DPLL may choose the input, so it has to be
+	 * in this DPLL's priority table -- ref->enabled only says the
+	 * receiver is powered, which is shared by every DPLL fed from the
+	 * same lane.  Signal quality is reported through the pin's own
+	 * attributes rather than by demoting the state, so a lane that is
+	 * routed here but momentarily in LOS stays selectable.
+	 */
+	else if (!chan->mode && (ref->pll_mask & BIT(sitdpll->id)))
+		*state = DPLL_PIN_STATE_SELECTABLE;
+	else
+		*state = DPLL_PIN_STATE_DISCONNECTED;
+
+	mutex_unlock(&sitdev->multiop_lock);
+
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_input_pin_state_on_dpll_set - set input pin DPLL state
+ *
+ * Enables or disables the physical input receiver via Page 0x02
+ * force/state registers (sit9531x_input_disable/enable()) and updates
+ * this DPLL's Page 1 priority table so the state is honoured by the
+ * PLL's automatic reference selection, not just at the input buffer.
+ *   DISCONNECTED -> drop the input from this DPLL's priority table and
+ *                  release this DPLL's claim on the input
+ *   SELECTABLE   -> claim the input for this DPLL and make it eligible
+ *                  again in the priority table
+ *   CONNECTED    -> same as SELECTABLE (the PLL auto-switch logic does
+ *                  the actual selection)
+ *
+ * The priority table is per PLL, so it is always updated for this DPLL.
+ * A single physical input feeds every DPLL, so the hardware receiver is
+ * only cut off once the last DPLL has released it: ref->pll_mask tracks
+ * which DPLLs currently claim the input, and the physical disable
+ * happens on the transition to an empty mask.
+ */
+static int
+sit9531x_dpll_input_pin_state_on_dpll_set(const struct dpll_pin *pin,
+					  void *pin_priv,
+					  const struct dpll_device *dpll,
+					  void *dpll_priv,
+					  enum dpll_pin_state state,
+					  struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	struct sit9531x_ref *ref = &sitdev->ref[dpin->id];
+	u8 hw_src = sit9531x_input_hw_src(dpin->id);
+	u8 pll_bit = BIT(sitdpll->id);
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	switch (state) {
+	case DPLL_PIN_STATE_DISCONNECTED:
+		rc = sit9531x_input_prio_remove(sitdev, sitdpll->id, hw_src);
+		if (rc)
+			break;
+		ref->pll_mask &= ~pll_bit;
+		if (ref->pll_mask)
+			rc = 0;	/* another DPLL still uses this input */
+		else
+			rc = sit9531x_input_disable(sitdev, dpin->id);
+		break;
+	case DPLL_PIN_STATE_CONNECTED:
+		/*
+		 * CONNECTED asks for this input and no other.  The chip has
+		 * no such mode: MANUAL disables the outer loop rather than
+		 * pinning a reference, so honouring the request would mean
+		 * emptying the priority table of every other source, which
+		 * is not what the caller asked for.  Refuse instead of
+		 * quietly behaving like SELECTABLE.
+		 */
+		NL_SET_ERR_MSG(extack,
+			       "Device selects its reference by priority; use selectable");
+		rc = -EOPNOTSUPP;
+		break;
+	case DPLL_PIN_STATE_SELECTABLE:
+		rc = sit9531x_input_enable(sitdev, dpin->id);
+		if (rc)
+			break;
+		rc = sit9531x_input_prio_add(sitdev, sitdpll->id, hw_src);
+		if (rc)
+			break;
+		/*
+		 * Claim the input for this DPLL only once it is both enabled
+		 * and present in the priority table.  Setting the mask before
+		 * prio_add would leak the claim if prio_add failed, keeping the
+		 * shared input receiver powered even after every DPLL released
+		 * it.
+		 */
+		ref->pll_mask |= pll_bit;
+		break;
+	default:
+		rc = -EINVAL;
+		break;
+	}
+
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc)
+		NL_SET_ERR_MSG(extack, "Failed to set input pin state");
+
+	return rc;
+}
+
+/*
+ * sit9531x_dpll_input_pin_prio_get - read input pin priority
+ *
+ * reads the PLL's priority table on Page 1 (via
+ * sit9531x_input_prio_get()) and returns the slot the input
+ * occupies, so pin-get reports the real hardware priority rather
+ * than a software default.
+ */
+static int
+sit9531x_dpll_input_pin_prio_get(const struct dpll_pin *pin, void *pin_priv,
+				 const struct dpll_device *dpll, void *dpll_priv,
+				 u32 *prio, struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	u8 slot;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_input_prio_get(sitdev, sitdpll->id,
+				     sit9531x_input_hw_src(dpin->id), &slot);
+	mutex_unlock(&sitdev->multiop_lock);
+	if (rc)
+		return rc;
+
+	dpin->prio = slot;
+	*prio = slot;
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_input_pin_prio_set - set input pin priority
+ *
+ * writes input priority table on Page 1 via
+ * core.c sit9531x_input_prio_set().  Forces holdover during update.
+ */
+static int
+sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
+				 const struct dpll_device *dpll, void *dpll_priv,
+				 u32 prio, struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	int rc;
+
+	if (dpin->dir != DPLL_PIN_DIRECTION_INPUT) {
+		NL_SET_ERR_MSG(extack, "Priority applies only to input pins");
+		return -EINVAL;
+	}
+
+	if (prio >= SIT9531X_PRIO_MAX_SLOTS) {
+		NL_SET_ERR_MSG(extack, "Priority out of range (0-10)");
+		return -EINVAL;
+	}
+
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_input_prio_set(sitdev, sitdpll->id,
+				     sit9531x_input_hw_src(dpin->id),
+				     (u8)prio);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc) {
+		NL_SET_ERR_MSG(extack, "Failed to set input priority");
+		return rc;
+	}
+
+	dpin->prio = (u8)prio;
+
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_input_pin_phase_adjust_get - read phase adjustment
+ *
+ * returns cached phase adjustment value (in ps).
+ */
+static int
+sit9531x_dpll_input_pin_phase_adjust_get(const struct dpll_pin *pin,
+					 void *pin_priv,
+					 const struct dpll_device *dpll,
+					 void *dpll_priv, s32 *phase_adjust,
+					 struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+
+	*phase_adjust = dpin->phase_adjust;
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_input_pin_phase_offset_get - read phase offset
+ *
+ * reads the TDC (Time-to-Digital Converter) hardware
+ * to measure the phase difference in picoseconds via
+ * sit9531x_phase_offset_read().
+ */
+/*
+ * sit9531x_dpll_input_pin_ffo_get - read the input's frequency offset
+ *
+ * The offset is derived from how far the PLL's running DIVN sits from
+ * its configured one, which only says something about the reference the
+ * PLL is actually tracking.  For every other input there is no
+ * measurement, and -ENODATA leaves the attribute out rather than
+ * reporting the active reference's figure against the wrong pin.
+ */
+static int
+sit9531x_dpll_input_pin_ffo_get(const struct dpll_pin *pin, void *pin_priv,
+				const struct dpll_device *dpll, void *dpll_priv,
+				struct dpll_ffo_param *ffo,
+				struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	int rc;
+
+	if (sitdev->chan[sitdpll->id].selected_ref != dpin->id)
+		return -ENODATA;
+
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_pll_ffo_ppt(sitdev, sitdpll->id, &ffo->ffo);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	return rc;
+}
+
+static int
+sit9531x_dpll_input_pin_phase_offset_get(const struct dpll_pin *pin,
+					 void *pin_priv,
+					 const struct dpll_device *dpll,
+					 void *dpll_priv, s64 *phase_offset,
+					 struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	s64 offset;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	/*
+	 * The on-chip TDC is a per-PLL resource that always measures the
+	 * phase difference between the VCO and the PLL's currently
+	 * selected reference; it cannot be pointed at an arbitrary input.
+	 * For any input that is not the active reference there is no
+	 * meaningful per-pin phase offset, so report 0 instead of the
+	 * active reference's value.
+	 */
+	if (sitdev->chan[sitdpll->id].selected_ref != dpin->id) {
+		mutex_unlock(&sitdev->multiop_lock);
+		dpin->phase_offset = 0;
+		*phase_offset = 0;
+		return 0;
+	}
+
+	rc = sit9531x_phase_offset_read(sitdev, sitdpll->id, &offset);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	/*
+	 * -ENODEV means the PLL has no programmed DIVN (unused on this
+	 * board); report phase_offset = 0 so a full pin-get dump does not
+	 * fail just because one DPLL is dormant.
+	 */
+	if (rc == -ENODEV) {
+		dpin->phase_offset = 0;
+		*phase_offset = 0;
+		return 0;
+	}
+	if (rc) {
+		NL_SET_ERR_MSG(extack, "TDC phase readback failed");
+		return rc;
+	}
+
+	/*
+	 * The ABI reports phase offset in units of 1/DPLL_PHASE_OFFSET_DIVIDER
+	 * picoseconds: the integer part of the attribute is the value divided
+	 * by the divider, the remainder is the fraction.  The TDC resolves one
+	 * VCO period (hundreds of picoseconds), so the fractional digits are
+	 * always zero here, but the magnitude still has to be scaled or every
+	 * reading would be reported a thousand times too small.
+	 */
+	offset *= DPLL_PHASE_OFFSET_DIVIDER;
+
+	dpin->phase_offset = offset;
+	*phase_offset = offset;
+	return 0;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
+	.direction_get		= sit9531x_dpll_input_pin_direction_get,
+	.frequency_get		= sit9531x_dpll_input_pin_frequency_get,
+	.state_on_dpll_get	= sit9531x_dpll_input_pin_state_on_dpll_get,
+	.state_on_dpll_set	= sit9531x_dpll_input_pin_state_on_dpll_set,
+	.prio_get		= sit9531x_dpll_input_pin_prio_get,
+	.prio_set		= sit9531x_dpll_input_pin_prio_set,
+	.phase_adjust_get	= sit9531x_dpll_input_pin_phase_adjust_get,
+	.phase_offset_get	= sit9531x_dpll_input_pin_phase_offset_get,
+	/*
+	 * The measurement compares the PLL's running feedback divider with
+	 * its configured one, so it describes the device's own reference
+	 * rather than a port rate.
+	 */
+	.supported_ffo		= BIT(DPLL_FFO_PIN_DEVICE),
+	.ffo_get		= sit9531x_dpll_input_pin_ffo_get,
+};
+
+/*
+ * INTSYNC pin ops
+ *
+ * INTSYNC is the chip's inter-PLL sync net: one PLL drives it and other
+ * PLLs may lock to it instead of to an external reference.  The two
+ * roles are exposed as two separate pins so neither overloads the other:
+ *
+ *   - a source (output) pin registered on every DPLL.  Connecting it on a
+ *     DPLL makes that DPLL drive INTSYNC; only one DPLL may drive it at a
+ *     time.  It has no priority ops -- driving the net is not a reference
+ *     selection.
+ *   - a destination (input) pin registered on every DPLL.  Connecting it
+ *     on a DPLL makes that DPLL eligible to lock to INTSYNC as a
+ *     reference, so it carries the priority ops.
+ */
+
+/* ---- INTSYNC source (output) pin ---- */
+
+/* The INTSYNC source pin is an output; its direction_get is defined below. */
+static int
+sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
+				       void *pin_priv,
+				       const struct dpll_device *dpll,
+				       void *dpll_priv,
+				       enum dpll_pin_direction *direction,
+				       struct netlink_ext_ack *extack);
+
+static int
+sit9531x_dpll_intsync_src_state_on_dpll_get(const struct dpll_pin *pin,
+					    void *pin_priv,
+					    const struct dpll_device *dpll,
+					    void *dpll_priv,
+					    enum dpll_pin_state *state,
+					    struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+
+	mutex_lock(&sitdev->multiop_lock);
+	if (sitdev->intsync_src == sitdpll->id)
+		*state = DPLL_PIN_STATE_CONNECTED;
+	else
+		*state = DPLL_PIN_STATE_DISCONNECTED;
+	mutex_unlock(&sitdev->multiop_lock);
+
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_intsync_src_state_on_dpll_set - drive INTSYNC from a PLL
+ *
+ *   CONNECTED    -> this PLL drives the INTSYNC net
+ *   DISCONNECTED -> stop driving INTSYNC if this PLL drives it
+ *
+ * SELECTABLE is rejected: driving the net is an explicit output routing,
+ * not an automatic-selection candidate, matching the regular output pin.
+ */
+static int
+sit9531x_dpll_intsync_src_state_on_dpll_set(const struct dpll_pin *pin,
+					    void *pin_priv,
+					    const struct dpll_device *dpll,
+					    void *dpll_priv,
+					    enum dpll_pin_state state,
+					    struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	int rc = 0;
+	u8 prio;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	switch (state) {
+	case DPLL_PIN_STATE_CONNECTED:
+		if (sitdev->intsync_src == sitdpll->id)
+			break;
+		if (sitdev->intsync_src >= 0) {
+			NL_SET_ERR_MSG(extack,
+				       "INTSYNC is already sourced by another PLL");
+			rc = -EBUSY;
+			break;
+		}
+		/*
+		 * A PLL that already lists INTSYNC among its references must
+		 * not also drive it: the destination side refuses the mirror
+		 * of this, and without the check here the net could be routed
+		 * back into the PLL feeding it.
+		 */
+		rc = sit9531x_input_prio_get(sitdev, sitdpll->id,
+					     sit9531x_input_hw_src(SIT9531X_INTSYNC_PIN_ID),
+					     &prio);
+		if (rc)
+			break;
+		if (prio < SIT9531X_PRIO_MAX_SLOTS) {
+			NL_SET_ERR_MSG(extack,
+				       "PLL selects INTSYNC as a reference; it cannot drive it");
+			rc = -EBUSY;
+			break;
+		}
+		rc = sit9531x_intsync_enable(sitdev, sitdpll->id);
+		if (!rc)
+			sitdev->intsync_src = sitdpll->id;
+		break;
+	case DPLL_PIN_STATE_DISCONNECTED:
+		if (sitdev->intsync_src != sitdpll->id)
+			break;
+		rc = sit9531x_intsync_disable(sitdev, sitdpll->id);
+		if (!rc)
+			sitdev->intsync_src = -1;
+		break;
+	default:
+		rc = -EINVAL;
+		break;
+	}
+
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc && rc != -EBUSY)
+		NL_SET_ERR_MSG(extack, "Failed to set INTSYNC source state");
+
+	return rc;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_intsync_src_pin_ops = {
+	.direction_get		= sit9531x_dpll_output_pin_direction_get,
+	.state_on_dpll_get	= sit9531x_dpll_intsync_src_state_on_dpll_get,
+	.state_on_dpll_set	= sit9531x_dpll_intsync_src_state_on_dpll_set,
+};
+
+/* ---- INTSYNC destination (input) pin ---- */
+
+/*
+ * sit9531x_dpll_intsync_dst_state_on_dpll_get - INTSYNC reference state
+ *
+ * CONNECTED when this PLL is locked to INTSYNC as its reference,
+ * SELECTABLE when it runs in automatic mode and a source PLL is driving
+ * INTSYNC, DISCONNECTED otherwise.  The PLL that drives INTSYNC can never
+ * be its own destination.
+ */
+static int
+sit9531x_dpll_intsync_dst_state_on_dpll_get(const struct dpll_pin *pin,
+					    void *pin_priv,
+					    const struct dpll_device *dpll,
+					    void *dpll_priv,
+					    enum dpll_pin_state *state,
+					    struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	const struct sit9531x_chan *chan;
+
+	chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
+
+	mutex_lock(&sitdev->multiop_lock);
+	if (sitdev->intsync_src == sitdpll->id)
+		*state = DPLL_PIN_STATE_DISCONNECTED;
+	else if (chan->locked && !chan->inner_lol &&
+		 chan->selected_ref == SIT9531X_INTSYNC_PIN_ID)
+		*state = DPLL_PIN_STATE_CONNECTED;
+	else if (!chan->mode && sitdev->intsync_src >= 0)
+		*state = DPLL_PIN_STATE_SELECTABLE;
+	else
+		*state = DPLL_PIN_STATE_DISCONNECTED;
+	mutex_unlock(&sitdev->multiop_lock);
+
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_intsync_dst_state_on_dpll_set - lock a PLL to INTSYNC
+ *
+ *   CONNECTED/SELECTABLE -> add INTSYNC to this PLL's priority table
+ *   DISCONNECTED         -> drop INTSYNC from this PLL's priority table
+ *
+ * INTSYNC is an internal net with no physical receiver, so only the
+ * per-PLL priority table is touched; the source pin controls generation.
+ */
+static int
+sit9531x_dpll_intsync_dst_state_on_dpll_set(const struct dpll_pin *pin,
+					    void *pin_priv,
+					    const struct dpll_device *dpll,
+					    void *dpll_priv,
+					    enum dpll_pin_state state,
+					    struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	u8 hw_src = sit9531x_input_hw_src(SIT9531X_INTSYNC_PIN_ID);
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	switch (state) {
+	case DPLL_PIN_STATE_DISCONNECTED:
+		rc = sit9531x_input_prio_remove(sitdev, sitdpll->id, hw_src);
+		break;
+	case DPLL_PIN_STATE_SELECTABLE:
+	case DPLL_PIN_STATE_CONNECTED:
+		if (sitdev->intsync_src == sitdpll->id) {
+			NL_SET_ERR_MSG(extack,
+				       "PLL cannot lock to the INTSYNC it drives");
+			rc = -EINVAL;
+			break;
+		}
+		rc = sit9531x_input_prio_add(sitdev, sitdpll->id, hw_src);
+		break;
+	default:
+		rc = -EINVAL;
+		break;
+	}
+
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc && rc != -EINVAL)
+		NL_SET_ERR_MSG(extack, "Failed to set INTSYNC input state");
+
+	return rc;
+}
+
+/*
+ * Do not add .frequency_get / the generic input state getter here: the
+ * destination pin id is SIT9531X_INTSYNC_PIN_ID, one past the end of the
+ * ref[] array (INTSYNC is an internal net with no ref[] entry).  The ops
+ * below only ever key on chan[] and the priority table, never ref[id].
+ */
+static const struct dpll_pin_ops sit9531x_dpll_intsync_dst_pin_ops = {
+	.direction_get		= sit9531x_dpll_input_pin_direction_get,
+	.state_on_dpll_get	= sit9531x_dpll_intsync_dst_state_on_dpll_get,
+	.state_on_dpll_set	= sit9531x_dpll_intsync_dst_state_on_dpll_set,
+	.prio_get		= sit9531x_dpll_input_pin_prio_get,
+	.prio_set		= sit9531x_dpll_input_pin_prio_set,
+};
+
+/*
+ * XO (crystal oscillator) pin ops
+ *
+ * The XO is the chip's internal reference oscillator that feeds every
+ * PLL.  It is exposed so userspace can see the on-chip reference, but it
+ * cannot be routed or disconnected, so it is reported permanently
+ * connected and offers no state_on_dpll_set / prio ops.
+ */
+
+static int
+sit9531x_dpll_xo_pin_state_on_dpll_get(const struct dpll_pin *pin,
+				       void *pin_priv,
+				       const struct dpll_device *dpll,
+				       void *dpll_priv,
+				       enum dpll_pin_state *state,
+				       struct netlink_ext_ack *extack)
+{
+	*state = DPLL_PIN_STATE_CONNECTED;
+	return 0;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_xo_pin_ops = {
+	.direction_get		= sit9531x_dpll_input_pin_direction_get,
+	.frequency_get		= sit9531x_dpll_input_pin_frequency_get,
+	.state_on_dpll_get	= sit9531x_dpll_xo_pin_state_on_dpll_get,
+};
+
+static int
+sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
+				       void *pin_priv,
+				       const struct dpll_device *dpll,
+				       void *dpll_priv,
+				       enum dpll_pin_direction *direction,
+				       struct netlink_ext_ack *extack)
+{
+	*direction = DPLL_PIN_DIRECTION_OUTPUT;
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_output_pin_frequency_get - read output pin frequency
+ *
+ * Reads the DIVO divider back from the chip and computes the live
+ * frequency as Fvco / DIVO.  Falls back to the cached value when the
+ * output is not resolvable through the divider chain (e.g. not mapped
+ * to a PLL), so a netlink dump never fails on such pins.
+ */
+static int
+sit9531x_dpll_output_pin_frequency_get(const struct dpll_pin *pin,
+				       void *pin_priv,
+				       const struct dpll_device *dpll,
+				       void *dpll_priv, u64 *frequency,
+				       struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_output_freq_get(sitdev, dpin->id, frequency);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc)
+		*frequency = sit9531x_out_state_get(sitdev, dpin->id)->freq;
+
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_output_pin_frequency_set - set output pin frequency
+ *
+ * computes DIVO = Fvco / frequency and writes the
+ * 34-bit output divider to the output system registers via
+ * sit9531x_output_freq_set().
+ */
+static int
+sit9531x_dpll_output_pin_frequency_set(const struct dpll_pin *pin,
+				       void *pin_priv,
+				       const struct dpll_device *dpll,
+				       void *dpll_priv, u64 frequency,
+				       struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	u8 actual_pll;
+	int rc;
+
+	/*
+	 * Read the PLL that drives this output from its OUT_MAP state
+	 * (populated by out_state_fetch from the chip's OUT_MAP registers).
+	 * That is the index the output register programming below is keyed
+	 * by; the output is registered under the DPLL matching this PLL.
+	 */
+	actual_pll = sitdev->out[dpin->id].pll_idx;
+
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll,
+				      frequency);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc)
+		NL_SET_ERR_MSG(extack, "Output frequency set failed");
+
+	return rc;
+}
+
+/*
+ * sit9531x_dpll_output_pin_state_on_dpll_get - get output pin state
+ *
+ * reports CONNECTED when the output is driven and
+ * DISCONNECTED when it has been muted via sit9531x_output_disable().
+ */
+static int
+sit9531x_dpll_output_pin_state_on_dpll_get(const struct dpll_pin *pin,
+					   void *pin_priv,
+					   const struct dpll_device *dpll,
+					   void *dpll_priv,
+					   enum dpll_pin_state *state,
+					   struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	const struct sit9531x_out *out;
+
+	out = sit9531x_out_state_get(sitdpll->dev, dpin->id);
+	*state = out->enabled ? DPLL_PIN_STATE_CONNECTED
+			      : DPLL_PIN_STATE_DISCONNECTED;
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_output_pin_state_on_dpll_set - mute/un-mute an output
+ *
+ * forces Hi-Z on the output pin via the Page 0x03
+ * force/state register pair.
+ *   CONNECTED    -> enable (release force, back to factory default)
+ *   DISCONNECTED -> disable (force Hi-Z)
+ */
+static int
+sit9531x_dpll_output_pin_state_on_dpll_set(const struct dpll_pin *pin,
+					   void *pin_priv,
+					   const struct dpll_device *dpll,
+					   void *dpll_priv,
+					   enum dpll_pin_state state,
+					   struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	switch (state) {
+	case DPLL_PIN_STATE_CONNECTED:
+		rc = sit9531x_output_enable(sitdev, dpin->id);
+		break;
+	case DPLL_PIN_STATE_DISCONNECTED:
+		rc = sit9531x_output_disable(sitdev, dpin->id);
+		break;
+	default:
+		rc = -EINVAL;
+		break;
+	}
+
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc)
+		NL_SET_ERR_MSG(extack, "Failed to set output pin state");
+
+	return rc;
+}
+
+/*
+ * sit9531x_dpll_output_pin_phase_adjust_get - read output phase adjustment
+ *
+ * returns cached value.
+ */
+static int
+sit9531x_dpll_output_pin_phase_adjust_get(const struct dpll_pin *pin,
+					  void *pin_priv,
+					  const struct dpll_device *dpll,
+					  void *dpll_priv, s32 *phase_adjust,
+					  struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+
+	*phase_adjust = dpin->phase_adjust;
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_output_pin_phase_adjust_set - set output phase adjustment
+ *
+ * Programs the per-output PRG_RST_DELAY registers for deterministic
+ * phase offset; see sit9531x_output_phase_adjust_set() in core.c.
+ */
+static int
+sit9531x_dpll_output_pin_phase_adjust_set(const struct dpll_pin *pin,
+					  void *pin_priv,
+					  const struct dpll_device *dpll,
+					  void *dpll_priv, s32 phase_adjust,
+					  struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_output_phase_adjust_set(sitdev, dpin->id, phase_adjust);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc) {
+		NL_SET_ERR_MSG(extack, "Phase adjust failed");
+		return rc;
+	}
+
+	dpin->phase_adjust = phase_adjust;
+	return 0;
+}
+
+static int
+sit9531x_dpll_output_pin_esync_get(const struct dpll_pin *pin,
+				   void *pin_priv,
+				   const struct dpll_device *dpll,
+				   void *dpll_priv,
+				   struct dpll_pin_esync *esync,
+				   struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+
+	if (!sit9531x_dpll_esync_pin_supported(dpin))
+		return -EOPNOTSUPP;
+
+	esync->range = sit9531x_esync_ranges;
+	esync->range_num = ARRAY_SIZE(sit9531x_esync_ranges);
+	esync->pulse = SIT9531X_ESYNC_PULSE_DEFAULT;
+	esync->freq = dpin->esync_freq;
+
+	return 0;
+}
+
+static int
+sit9531x_dpll_output_pin_esync_set(const struct dpll_pin *pin,
+				   void *pin_priv,
+				   const struct dpll_device *dpll,
+				   void *dpll_priv,
+				   u64 freq,
+				   struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	u8 actual_pll;
+	int rc;
+
+	if (!sit9531x_dpll_esync_pin_supported(dpin)) {
+		NL_SET_ERR_MSG(extack,
+			       "Embedded sync not enabled for this pin");
+		return -EOPNOTSUPP;
+	}
+
+	actual_pll = sitdev->out[dpin->id].pll_idx;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	/*
+	 * This output is a dedicated embedded-sync pin.
+	 * Treat freq=0 as a request to disable the entire output.
+	 */
+	if (!freq) {
+		rc = sit9531x_output_disable(sitdev, dpin->id);
+		if (!rc)
+			dpin->esync_freq = 0;
+		mutex_unlock(&sitdev->multiop_lock);
+		return rc;
+	}
+
+	if (freq != SIT9531X_ESYNC_FREQ_10MHZ) {
+		mutex_unlock(&sitdev->multiop_lock);
+		NL_SET_ERR_MSG(extack,
+			       "Only 10 MHz esync frequency is supported");
+		return -EINVAL;
+	}
+
+	rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll,
+				      SIT9531X_ESYNC_FREQ_10MHZ);
+	/*
+	 * Program the pulse generator (PROG0 PULSE_CTRL) so the embedded-sync
+	 * pulse is actually emitted; without it the output carries the clock
+	 * but no esync marker.  SIT9531X_ESYNC_PULSE_DEFAULT is the same duty
+	 * the esync_get callback advertises.
+	 */
+	if (!rc)
+		rc = sit9531x_output_pulse_ctrl_set(sitdev, dpin->id,
+						    SIT9531X_ESYNC_PULSE_DEFAULT);
+	if (!rc)
+		rc = sit9531x_output_enable(sitdev, dpin->id);
+
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (!rc)
+		dpin->esync_freq = SIT9531X_ESYNC_FREQ_10MHZ;
+
+	return rc;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_output_pin_ops = {
+	.direction_get		= sit9531x_dpll_output_pin_direction_get,
+	.frequency_get		= sit9531x_dpll_output_pin_frequency_get,
+	.frequency_set		= sit9531x_dpll_output_pin_frequency_set,
+	.state_on_dpll_get	= sit9531x_dpll_output_pin_state_on_dpll_get,
+	.state_on_dpll_set	= sit9531x_dpll_output_pin_state_on_dpll_set,
+	.phase_adjust_get	= sit9531x_dpll_output_pin_phase_adjust_get,
+	.phase_adjust_set	= sit9531x_dpll_output_pin_phase_adjust_set,
+	.esync_get		= sit9531x_dpll_output_pin_esync_get,
+	.esync_set		= sit9531x_dpll_output_pin_esync_set,
+};
+
+const struct dpll_pin_ops *
+sit9531x_dpll_pin_ops_get(const struct sit9531x_dpll_pin *pin)
+{
+	if (!sit9531x_dpll_is_input_pin(pin)) {
+		if (sit9531x_dpll_is_intsync_src_pin(pin))
+			return &sit9531x_dpll_intsync_src_pin_ops;
+		return &sit9531x_dpll_output_pin_ops;
+	}
+	if (sit9531x_dpll_is_intsync_pin(pin))
+		return &sit9531x_dpll_intsync_dst_pin_ops;
+	if (sit9531x_dpll_is_xo_pin(pin))
+		return &sit9531x_dpll_xo_pin_ops;
+	return &sit9531x_dpll_input_pin_ops;
+}
+
+/*
+ * sit9531x_dpll_changes_check - check for state changes and notify
+ *
+ * Called from sit9531x_dev_periodic_work().  Compares current hardware
+ * state against cached values and sends netlink notifications on changes.
+ */
+void sit9531x_dpll_changes_check(struct sit9531x_dpll *sitdpll)
+{
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	enum dpll_lock_status lock_status;
+	struct sit9531x_dpll_pin *pin;
+	int rc;
+
+	rc = sit9531x_dpll_lock_status_get(sitdpll->dpll_dev, sitdpll,
+					   &lock_status, NULL, NULL);
+	if (rc) {
+		dev_err(sitdev->dev, "Failed to get DPLL%u lock status: %d\n",
+			sitdpll->id, rc);
+		return;
+	}
+
+	/* If lock status changed, notify DPLL core */
+	if (sitdpll->lock_status != lock_status) {
+		sitdpll->lock_status = lock_status;
+		dpll_device_change_ntf(sitdpll->dpll_dev);
+	}
+
+	list_for_each_entry(pin, &sitdpll->pins, list) {
+		const struct dpll_pin_ops *ops;
+		enum dpll_pin_state state;
+
+		/*
+		 * Poll input pins whose state can change autonomously: regular
+		 * references and the INTSYNC destination pin.  Outputs (incl.
+		 * the INTSYNC source) change only through their own set
+		 * callback and the XO is permanently connected, so skip those.
+		 * Each pin's own state_on_dpll_get resolves to the right getter.
+		 */
+		if (!sit9531x_dpll_is_input_pin(pin) ||
+		    sit9531x_dpll_is_xo_pin(pin))
+			continue;
+
+		ops = sit9531x_dpll_pin_ops_get(pin);
+		rc = ops->state_on_dpll_get(pin->dpll_pin, pin,
+					    sitdpll->dpll_dev, sitdpll,
+					    &state, NULL);
+		if (rc)
+			continue;
+
+		if (state != pin->pin_state) {
+			dev_dbg(sitdev->dev, "%s state changed: %u->%u\n",
+				pin->label, pin->pin_state, state);
+			pin->pin_state = state;
+			dpll_pin_change_ntf(pin->dpll_pin);
+		}
+	}
+}
diff --git a/drivers/dpll/sit9531x/dpll.h b/drivers/dpll/sit9531x/dpll.h
new file mode 100644
index 000000000000..e5eef4514bcd
--- /dev/null
+++ b/drivers/dpll/sit9531x/dpll.h
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * SiTime SiT9531x DPLL subsystem interface
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * DPLL device and pin structures, and function declarations for
+ * the DPLL registration and callback layer.
+ */
+
+#ifndef _SIT9531X_DPLL_H
+#define _SIT9531X_DPLL_H
+
+#include <linux/dpll.h>
+#include <linux/list.h>
+#include <linux/types.h>
+
+struct sit9531x_dev;
+
+/* Per-pin DPLL state. */
+struct sit9531x_dpll_pin {
+	struct list_head		list;
+	struct sit9531x_dpll		*dpll;
+	struct dpll_pin			*dpll_pin;
+	dpll_tracker			tracker;
+	struct fwnode_handle		*fwnode;
+	char				label[8];	/* "IN0", "OUT3" */
+	enum dpll_pin_direction		dir;
+	u8				id;		/* hardware index */
+	u8				prio;
+	enum dpll_pin_state		pin_state;
+	s32				phase_adjust;	/* picoseconds */
+	s64				phase_offset;	/* picoseconds */
+	bool				esync_control;
+	u64				esync_freq;	/* 0 == disabled */
+};
+
+/* Per-PLL DPLL device state. */
+struct sit9531x_dpll {
+	struct list_head		list;
+	struct sit9531x_dev		*dev;
+	struct dpll_device		*dpll_dev;
+	dpll_tracker			tracker;
+	struct dpll_device_ops		ops;	/* per-instance copy */
+	struct list_head		pins;
+	u8				id;	/* 0 = PLLA .. 3 = PLLD */
+	enum dpll_lock_status		lock_status;
+};
+
+/* ---- DPLL allocation and registration ---- */
+/*
+ * The callback tables stay with the callbacks; the registration code that
+ * hands them to the subsystem lives next to probe() in core.c.
+ */
+extern const struct dpll_device_ops sit9531x_dpll_device_ops;
+const struct dpll_pin_ops *
+sit9531x_dpll_pin_ops_get(const struct sit9531x_dpll_pin *pin);
+
+struct sit9531x_dpll *sit9531x_dpll_alloc(struct sit9531x_dev *sitdev, u8 ch);
+void sit9531x_dpll_free(struct sit9531x_dpll *sitdpll);
+int  sit9531x_dpll_register(struct sit9531x_dpll *sitdpll);
+void sit9531x_dpll_unregister(struct sit9531x_dpll *sitdpll);
+
+/* ---- Periodic change detection ---- */
+void sit9531x_dpll_changes_check(struct sit9531x_dpll *sitdpll);
+
+#endif /* _SIT9531X_DPLL_H */
diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
new file mode 100644
index 000000000000..8a0105c8c647
--- /dev/null
+++ b/drivers/dpll/sit9531x/prop.c
@@ -0,0 +1,397 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * SiTime SiT9531x firmware node property parsing
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * Retrieves pin properties from Device Tree firmware nodes (or
+ * applies defaults when no firmware node exists).
+ */
+
+#include <linux/dev_printk.h>
+#include <linux/dpll.h>
+#include <linux/err.h>
+#include <linux/fwnode.h>
+#include <linux/property.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#include "core.h"
+#include "prop.h"
+
+/*
+ * sit9531x_input_pin_label - fill the package label for an input pin
+ *
+ * Split out so input-naming changes stay local to this helper.
+ */
+static void sit9531x_input_pin_label(struct sit9531x_dev *sitdev,
+				     struct sit9531x_pin_props *props, u8 id)
+{
+	u8 pair = sit9531x_input_pair(id);
+
+	if (sitdev->ref[id].sig_mode == SIT9531X_MODE_DE)
+		snprintf(props->package_label,
+			 sizeof(props->package_label), "IN%u", pair);
+	else
+		snprintf(props->package_label,
+			 sizeof(props->package_label), "IN%u%c", pair,
+			 sit9531x_input_is_n(id) ? 'N' : 'P');
+}
+
+/*
+ * sit9531x_prop_pin_package_label_set - generate package label
+ * @dir:	pin direction
+ * @id:		pin index
+ *
+ * Generates a package label string.  Output pins are named "OUT0",
+ * "OUT1", ...  Input pins are named after the physical pair and lane:
+ * "IN0P", "IN0N", "IN1P", ... for single-ended lanes, or "IN0",
+ * "IN1", ... when the pair is configured differential (the N lane is
+ * not registered in that case).
+ */
+static void
+sit9531x_prop_pin_package_label_set(struct sit9531x_dev *sitdev,
+				    struct sit9531x_pin_props *props,
+				    enum dpll_pin_direction dir, u8 id)
+{
+	/* The internal INTSYNC pin has a fixed label */
+	if (dir == DPLL_PIN_DIRECTION_INPUT &&
+	    id == SIT9531X_INTSYNC_PIN_ID) {
+		strscpy(props->package_label, "INTSYNC",
+			sizeof(props->package_label));
+		props->dpll_props.package_label = props->package_label;
+		return;
+	}
+
+	/* The internal XO reference has a fixed label */
+	if (dir == DPLL_PIN_DIRECTION_INPUT && id == SIT9531X_MAX_INPUTS) {
+		strscpy(props->package_label, "XO",
+			sizeof(props->package_label));
+		props->dpll_props.package_label = props->package_label;
+		return;
+	}
+
+	/* The internal INTSYNC source (output) pin has a fixed label */
+	if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+	    id == SIT9531X_INTSYNC_OUT_PIN_ID) {
+		strscpy(props->package_label, "SYNCOUT",
+			sizeof(props->package_label));
+		props->dpll_props.package_label = props->package_label;
+		return;
+	}
+
+	if (dir == DPLL_PIN_DIRECTION_INPUT)
+		sit9531x_input_pin_label(sitdev, props, id);
+	else
+		snprintf(props->package_label, sizeof(props->package_label),
+			 "OUT%u", id);
+
+	props->dpll_props.package_label = props->package_label;
+}
+
+/*
+ * sit9531x_prop_pin_fwnode_get - find firmware node for a pin
+ * @dir:	pin direction
+ * @id:		pin index
+ *
+ * Searches for input-pins/output-pins child nodes in DT, looking
+ * for a child whose "reg" property matches @id.
+ *
+ * Return: 0 on success, -ENOENT if no firmware node exists
+ */
+static int
+sit9531x_prop_pin_fwnode_get(struct sit9531x_dev *sitdev,
+			     struct sit9531x_pin_props *props,
+			     enum dpll_pin_direction dir, u8 id)
+{
+	struct fwnode_handle *pins_node, *pin_node;
+	const char *node_name;
+
+	if (dir == DPLL_PIN_DIRECTION_INPUT)
+		node_name = "input-pins";
+	else
+		node_name = "output-pins";
+
+	pins_node = device_get_named_child_node(sitdev->dev, node_name);
+	if (!pins_node) {
+		dev_dbg(sitdev->dev, "'%s' sub-node is missing\n", node_name);
+		return -ENOENT;
+	}
+
+	/* Enumerate child pin nodes and find the requested one */
+	fwnode_for_each_child_node(pins_node, pin_node) {
+		u32 reg;
+
+		if (fwnode_property_read_u32(pin_node, "reg", &reg))
+			continue;
+
+		if (id == reg)
+			break;
+	}
+
+	fwnode_handle_put(pins_node);
+
+	props->fwnode = pin_node;
+
+	dev_dbg(sitdev->dev, "Firmware node for %s %sfound\n",
+		props->package_label, pin_node ? "" : "NOT ");
+
+	return pin_node ? 0 : -ENOENT;
+}
+
+/*
+ * sit9531x_pin_props_get - get pin properties for a given pin
+ * @dir:	pin direction (INPUT or OUTPUT)
+ * @index:	pin index
+ *
+ * Allocates a pin properties structure, generates a package label,
+ * looks up the firmware node if available, and reads optional
+ * properties (label, connection-type, supported-frequencies-hz,
+ * esync-control).
+ *
+ * Call sit9531x_pin_props_put() to free the returned structure.
+ *
+ * Return: pointer to pin properties on success, error pointer on error
+ */
+struct sit9531x_pin_props *
+sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
+		       enum dpll_pin_direction dir, u8 index)
+{
+	struct dpll_pin_frequency *ranges;
+	struct sit9531x_pin_props *props;
+	int i, j, num_freqs = 0, rc;
+	u64 *freqs = NULL;
+	const char *type;
+	u32 curr_freq;
+
+	props = kzalloc_obj(*props, GFP_KERNEL);
+	if (!props)
+		return ERR_PTR(-ENOMEM);
+
+	if (dir == DPLL_PIN_DIRECTION_INPUT &&
+	    index == SIT9531X_INTSYNC_PIN_ID) {
+		/*
+		 * INTSYNC destination pin: a PLL locks to the INTSYNC net as a
+		 * reference, so it can be connected and re-prioritised.
+		 */
+		props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+		props->dpll_props.capabilities =
+			DPLL_PIN_CAPABILITIES_PRIORITY_CAN_CHANGE |
+			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+		curr_freq = 0;
+	} else if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+		   index == SIT9531X_INTSYNC_OUT_PIN_ID) {
+		/*
+		 * INTSYNC source pin: a PLL drives the INTSYNC net.  It can be
+		 * connected/disconnected but carries no priority (driving the
+		 * net is not a reference selection) and no frequency.
+		 */
+		props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+		props->dpll_props.capabilities =
+			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+		curr_freq = 0;
+	} else if (dir == DPLL_PIN_DIRECTION_INPUT &&
+		   index == SIT9531X_MAX_INPUTS) {
+		/* The XO reference is fixed: no state or priority control. */
+		props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+		props->dpll_props.capabilities = 0;
+		sitdev->ref[index].freq = sitdev->xtal_freq;
+		curr_freq = sitdev->xtal_freq;
+	} else if (dir == DPLL_PIN_DIRECTION_INPUT) {
+		props->dpll_props.type = DPLL_PIN_TYPE_EXT;
+		props->dpll_props.capabilities =
+			DPLL_PIN_CAPABILITIES_PRIORITY_CAN_CHANGE |
+			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+		curr_freq = sitdev->ref[index].freq;
+	} else {
+		props->dpll_props.type = DPLL_PIN_TYPE_GNSS;
+		props->dpll_props.capabilities =
+			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+		curr_freq = sitdev->out[index].freq;
+	}
+
+	/* Allow phase-adjust over +/-1 ms window.  The subsystem rejects
+	 * pin_set(phase-adjust, X) when X falls outside [min, max], so
+	 * leaving these at 0 silently blocks every netlink call.  1 ms is
+	 * well beyond the DCO dynamic range but costs nothing.
+	 */
+	props->dpll_props.phase_range.min = -1000000000; /* -1 ms in ps */
+	props->dpll_props.phase_range.max =  1000000000; /* +1 ms in ps */
+
+	/* Generate package label */
+	sit9531x_prop_pin_package_label_set(sitdev, props, dir, index);
+
+	/*
+	 * Both INTSYNC pins are internal to the chip and have no board-level
+	 * wiring, so they take no properties from the firmware node.
+	 */
+	if (dir == DPLL_PIN_DIRECTION_INPUT &&
+	    index == SIT9531X_INTSYNC_PIN_ID)
+		goto skip_fwnode_props;
+	if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+	    index == SIT9531X_INTSYNC_OUT_PIN_ID)
+		goto skip_fwnode_props;
+
+	rc = sit9531x_prop_pin_fwnode_get(sitdev, props, dir, index);
+	if (rc)
+		goto skip_fwnode_props;
+
+	/* Look for "label" property -> board label */
+	fwnode_property_read_string(props->fwnode, "label",
+				    &props->dpll_props.board_label);
+
+	/* Look for "connection-type" property -> pin type enum */
+	if (!fwnode_property_read_string(props->fwnode, "connection-type",
+					 &type)) {
+		if (!strcmp(type, "ext"))
+			props->dpll_props.type = DPLL_PIN_TYPE_EXT;
+		else if (!strcmp(type, "gnss"))
+			props->dpll_props.type = DPLL_PIN_TYPE_GNSS;
+		else if (!strcmp(type, "int") ||
+			 !strcmp(type, "int-oscillator"))
+			props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+		else if (!strcmp(type, "synce") ||
+			 !strcmp(type, "synce-eth-port"))
+			props->dpll_props.type = DPLL_PIN_TYPE_SYNCE_ETH_PORT;
+		else if (!strcmp(type, "mux"))
+			props->dpll_props.type = DPLL_PIN_TYPE_MUX;
+		else
+			dev_warn(sitdev->dev,
+				 "Unknown pin type '%s'\n", type);
+	}
+
+	props->esync_control =
+		fwnode_property_read_bool(props->fwnode, "esync-control");
+
+	num_freqs = fwnode_property_count_u64(props->fwnode,
+					      "supported-frequencies-hz");
+	if (num_freqs <= 0) {
+		num_freqs = 0;
+		goto skip_fwnode_props;
+	}
+
+	freqs = kcalloc(num_freqs, sizeof(*freqs), GFP_KERNEL);
+	if (!freqs) {
+		rc = -ENOMEM;
+		goto err_alloc_freqs;
+	}
+
+	fwnode_property_read_u64_array(props->fwnode,
+				       "supported-frequencies-hz",
+				       freqs, num_freqs);
+
+	/*
+	 * Seed the runtime ref->freq / out->freq with the first DT-listed
+	 * supported frequency so the netlink frequency_get callback reports
+	 * a sane initial value before any pin_set occurs.  DT lists the
+	 * physically-wired reference frequency for each input pin and the
+	 * default output frequency for each output pin.
+	 */
+	if (num_freqs > 0) {
+		if (dir == DPLL_PIN_DIRECTION_INPUT)
+			sitdev->ref[index].freq = (u32)freqs[0];
+		else
+			sitdev->out[index].freq = (u32)freqs[0];
+		curr_freq = (u32)freqs[0];
+	}
+
+skip_fwnode_props:
+	/* Neither INTSYNC pin carries a frequency attribute */
+	if (dir == DPLL_PIN_DIRECTION_INPUT &&
+	    index == SIT9531X_INTSYNC_PIN_ID)
+		return props;
+	if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+	    index == SIT9531X_INTSYNC_OUT_PIN_ID)
+		return props;
+
+	/* Allocate frequency ranges list -- DT discrete entries + current
+	 * freq + one catch-all wide range so the subsystem never pre-
+	 * rejects a frequency_set call.  The chip's real admissible set
+	 * is bounded by VCO / divider math in sit9531x_output_freq_set().
+	 */
+	ranges = kcalloc(num_freqs + 2, sizeof(*ranges), GFP_KERNEL);
+	if (!ranges) {
+		rc = -ENOMEM;
+		goto err_alloc_ranges;
+	}
+
+	/* Current freq as first entry */
+	ranges[0] = (struct dpll_pin_frequency)DPLL_PIN_FREQUENCY(curr_freq);
+	j = 1;
+
+	for (i = 0; i < num_freqs; i++) {
+		struct dpll_pin_frequency freq = DPLL_PIN_FREQUENCY(freqs[i]);
+
+		if (freqs[i] == curr_freq)
+			continue;
+		ranges[j++] = freq;
+	}
+
+	/* Always append a wide catch-all range */
+	ranges[j].min = 1;
+	ranges[j].max = 1000000000ULL; /* 1 GHz */
+	j++;
+
+	props->dpll_props.freq_supported = ranges;
+	props->dpll_props.freq_supported_num = j;
+
+	kfree(freqs);
+
+	return props;
+
+err_alloc_ranges:
+	kfree(freqs);
+err_alloc_freqs:
+	fwnode_handle_put(props->fwnode);
+	kfree(props);
+
+	return ERR_PTR(rc);
+}
+
+/*
+ * sit9531x_pin_props_put - release pin properties
+ * @props:	pin properties to free
+ */
+void sit9531x_pin_props_put(struct sit9531x_pin_props *props)
+{
+	kfree(props->dpll_props.freq_supported);
+
+	if (props->fwnode)
+		fwnode_handle_put(props->fwnode);
+
+	kfree(props);
+}
+
+/*
+ * sit9531x_prop_dpll_type_get - get DPLL channel type from firmware
+ * @index:	DPLL channel index (0-3)
+ *
+ * Reads the "dpll-types" string array property from the firmware node
+ * and returns the corresponding DPLL type enum.
+ *
+ * Return: DPLL type for the given channel (default: DPLL_TYPE_PPS)
+ */
+enum dpll_type
+sit9531x_prop_dpll_type_get(struct sit9531x_dev *sitdev, u8 index)
+{
+	const char *types[SIT9531X_NUM_PLLS];
+	int count;
+
+	count = device_property_read_string_array(sitdev->dev, "dpll-types",
+						  types, ARRAY_SIZE(types));
+
+	if (index >= count)
+		return DPLL_TYPE_PPS;
+
+	if (!strcmp(types[index], "pps"))
+		return DPLL_TYPE_PPS;
+	else if (!strcmp(types[index], "eec"))
+		return DPLL_TYPE_EEC;
+
+	dev_warn(sitdev->dev, "Unknown DPLL type '%s', using default\n",
+		 types[index]);
+
+	return DPLL_TYPE_PPS;
+}
diff --git a/drivers/dpll/sit9531x/prop.h b/drivers/dpll/sit9531x/prop.h
new file mode 100644
index 000000000000..f7f1c854b955
--- /dev/null
+++ b/drivers/dpll/sit9531x/prop.h
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * SiTime SiT9531x firmware node property parsing
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ */
+
+#ifndef _SIT9531X_PROP_H
+#define _SIT9531X_PROP_H
+
+#include <linux/dpll.h>
+#include <linux/fwnode.h>
+
+struct sit9531x_dev;
+
+/*
+ * struct sit9531x_pin_props - pin properties from firmware
+ * @fwnode:		firmware node handle (NULL if no DT node)
+ * @dpll_props:		DPLL core pin properties
+ * @package_label:	pin package label (e.g. "IN0", "OUT3")
+ * @esync_control:	embedded sync is controllable
+ */
+struct sit9531x_pin_props {
+	struct fwnode_handle		*fwnode;
+	struct dpll_pin_properties	dpll_props;
+	char				package_label[8];
+	bool				esync_control;
+};
+
+enum dpll_type sit9531x_prop_dpll_type_get(struct sit9531x_dev *sitdev,
+					   u8 index);
+struct sit9531x_pin_props *sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
+						  enum dpll_pin_direction dir,
+						  u8 index);
+void sit9531x_pin_props_put(struct sit9531x_pin_props *props);
+
+#endif /* _SIT9531X_PROP_H */
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
new file mode 100644
index 000000000000..5c3dbeefd86b
--- /dev/null
+++ b/drivers/dpll/sit9531x/regs.h
@@ -0,0 +1,371 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * SiTime SiT9531x register definitions
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ */
+
+#ifndef _SIT9531X_REGS_H
+#define _SIT9531X_REGS_H
+
+/*
+ * I2C register model:
+ *   - Page select register at offset 0x01
+ *   - Each page has 128 registers (0x00-0x7F)
+ *   - Some pages are paired (e.g. 0x0A/0x1A for PLLA)
+ */
+#define SIT9531X_PAGE_SEL		0xFF
+#define SIT9531X_PAGE_SIZE		0x100
+#define SIT9531X_NUM_PAGES		32
+
+/* Helper macros for page:offset addressing */
+#define SIT9531X_REG(_page, _offset)		(((_page) << 8) | (_offset))
+#define SIT9531X_REG_PAGE(_reg)		((_reg) >> 8)
+#define SIT9531X_REG_OFFSET(_reg)		((_reg) & 0xFF)
+
+/* ---- Page definitions ---- */
+#define SIT9531X_PAGE_MAINSYS0		0x00
+#define SIT9531X_PAGE_MAINSYS1		0x01
+#define SIT9531X_PAGE_INPUTSYS		0x02
+#define SIT9531X_PAGE_OUTSYS0		0x03
+#define SIT9531X_PAGE_OUTSYS1		0x04
+#define SIT9531X_PAGE_CLKMON0		0x06
+#define SIT9531X_PAGE_CLKMON1		0x07
+#define SIT9531X_PAGE_PLLA			0x0A
+#define SIT9531X_PAGE_PLLA_EXT		0x1A
+#define SIT9531X_PAGE_PLLB			0x0B
+#define SIT9531X_PAGE_PLLB_EXT		0x1B
+#define SIT9531X_PAGE_PLLC			0x0C
+#define SIT9531X_PAGE_PLLC_EXT		0x1C
+#define SIT9531X_PAGE_PLLD			0x0D
+#define SIT9531X_PAGE_PLLD_EXT		0x1D
+
+/* PLL index to page mapping */
+#define SIT9531X_PLL_PAGE(_idx) \
+	(SIT9531X_PAGE_PLLA + (_idx))
+
+/*
+ * VARIANT_ID is a single byte at page 0 reg 0x02 (95317 = 0x17, 95316 = 0x31).
+ * Reg 0x03 carries an unrelated revision byte and must not be combined into
+ * the variant identifier.
+ */
+#define SIT9531X_REG_VARIANT_ID		SIT9531X_REG(0x00, 0x02)
+
+/* DCO trigger pulse timing: minimum 6 ns required by hardware */
+
+#define SIT9531X_REG_HOLDOVER_HISTORY	SIT9531X_REG(0x00, 0x58)
+
+/* Page 0 -- PLL inner loop loss-of-lock */
+#define SIT9531X_REG_PLL_INNER_LOL_STATUS	SIT9531X_REG(0x00, 0x92)
+#define SIT9531X_REG_PLL_INNER_LOL_NOTIF	SIT9531X_REG(0x00, 0x93)
+
+/* Page 0 -- Clock monitor PLL / XO status */
+#define SIT9531X_REG_CMON_NOTIF		SIT9531X_REG(0x00, 0x9E)
+
+/* Page 0 -- PLL outer-loop loss-of-lock */
+#define SIT9531X_REG_OUTER_LOL_STATUS	SIT9531X_REG(0x00, 0x06)
+#define SIT9531X_REG_OUTER_LOL_NOTIF		SIT9531X_REG(0x00, 0x07)
+
+/* Page 0 -- PLL holdover freeze status */
+#define SIT9531X_REG_HO_FREEZE_STATUS	SIT9531X_REG(0x00, 0x0A)
+#define SIT9531X_REG_HO_FREEZE_NOTIF	SIT9531X_REG(0x00, 0x0B)
+
+/* Page 0 -- INTSYNC (inter-PLL synchronization) global enable */
+#define SIT9531X_REG_INTSYNC_GLOBAL		SIT9531X_REG(0x00, 0x40)
+#define SIT9531X_INTSYNC_EN_BIT		6
+
+/*
+ * Priority table: 6 registers per PLL, each holds two priority slots
+ * nibble-packed.  The register holding slots 2n and 2n+1 keeps the
+ * earlier slot (CLK_SPARE<2n>SEL_PLL) in [7:4] and the later one in
+ * [3:0].
+ *
+ * Base registers for PLLA: 0x16-0x1B (slots 0-10 plus the
+ * active-reference nibble).
+ * For PLL N:  base + 6 * N  (e.g. PLLB starts at 0x1C).
+ *
+ * Input source encoding (4-bit value):
+ *   0=IN0P, 1=IN1P, 2=IN2P, 3=IN3P, 4=IN4P,
+ *   5=OCXO, 6=INTSYNC,
+ *   7=IN0N, 8=IN1N, 9=IN2N, 10=IN3N, 11=IN4N
+ */
+#define SIT9531X_PAGE_PRIOSYS		0x01
+#define SIT9531X_PRIO_BASE_REG		0x16
+#define SIT9531X_PRIO_REGS_PER_PLL		6
+#define SIT9531X_PRIO_SLOTS_PER_REG		2
+/*
+ * 11 priority slots, CLK_SPARE0SEL_PLL through CLK_SPARE10SEL_PLL.
+ * The twelfth nibble of the block is not a slot: it is
+ * CLK_ACTIVESEL_PLL, see SIT9531X_PRIO_ACTIVESEL_OFF below.
+ */
+#define SIT9531X_PRIO_MAX_SLOTS		11
+/* Number of source encodings (0-11), unrelated to the slot count */
+#define SIT9531X_PRIO_NUM_SRC		12
+#define SIT9531X_PRIO_NIBBLE_MASK		0x0F
+#define SIT9531X_PRIO_HI_SHIFT		4
+/* Input source encoding values (see table above) */
+#define SIT9531X_PRIO_SRC_OCXO		5
+#define SIT9531X_PRIO_SRC_INTSYNC		6
+#define SIT9531X_PRIO_SRC_N_BASE		7
+/*
+ * The last register of each PLL's priority block holds, in its low
+ * nibble, the input source the PLL has currently selected as its
+ * active reference (CLK_ACTIVESEL_PLL, same 4-bit encoding as above).
+ */
+#define SIT9531X_PRIO_ACTIVESEL_OFF		5
+
+/*
+ * Page 0 -- PRG_Directives_GENERIC_0, the main system's programming
+ * directive register.  Every page carries its own copy of this
+ * register at offset 0x0F with the same bit layout:
+ *
+ *   bit 6  proceed to loop lock / active state from the PRG_CMD state
+ *   bit 4  update the NVM bank from the efuse contents
+ *   bit 3  read the efuse
+ *   bit 2  program the efuse
+ *   bit 1  small change update (SIT9531X_SMALL_UPDATE_CMD)
+ *   bit 0  escape to the PRG_CMD state
+ *
+ * Only the small change update belongs in a runtime path; the efuse
+ * and NVM directives touch non-volatile storage.
+ */
+#define SIT9531X_REG_GLOBAL_UPDATE		SIT9531X_REG(0x00, 0x0F)
+#define SIT9531X_SMALL_UPDATE_CMD		0x02
+
+/* PLL holdover control (PLL page offset) */
+#define SIT9531X_PLL_REG_HO_CTRL		0x6F
+#define SIT9531X_PLL_HO_FORCE_BIT		4
+
+/* One bit per input PAIR (bit 0 = CLKIN0, ..., bit 3 = CLKIN3) */
+#define SIT9531X_REG_IN_DE_FORCE		SIT9531X_REG(0x02, 0xE8)
+#define SIT9531X_REG_IN_DE_STATE		SIT9531X_REG(0x02, 0xE9)
+#define SIT9531X_REG_IN_SEP_FORCE		SIT9531X_REG(0x02, 0xEA)
+#define SIT9531X_REG_IN_SEP_STATE		SIT9531X_REG(0x02, 0xEB)
+#define SIT9531X_REG_IN_SEN_FORCE		SIT9531X_REG(0x02, 0xF2)
+#define SIT9531X_REG_IN_SEN_STATE		SIT9531X_REG(0x02, 0xF3)
+
+/*
+ * One register per input pair at 0x1B + 0x10 * pair
+ * (CLKIN0 = 0x1B, CLKIN1 = 0x2B, CLKIN2 = 0x3B, CLKIN3 = 0x4B).
+ * SE_P_EN/SE_N_EN set means the corresponding lane is configured
+ * single-ended; both clear means the pair runs differential.
+ */
+#define SIT9531X_REG_IN_MODE(_pair)		\
+	SIT9531X_REG(0x02, 0x1B + 0x10 * (_pair))
+#define SIT9531X_IN_MODE_SE_P_EN		BIT(0)
+#define SIT9531X_IN_MODE_SE_N_EN		BIT(1)
+
+/* ---- Page 0x03 (Output System) registers -- Hi-Z control ---- */
+#define SIT9531X_REG_HIZ_DIFF_07_MASK	SIT9531X_REG(0x03, 0xF2)
+#define SIT9531X_REG_HIZ_DIFF_07_STATE	SIT9531X_REG(0x03, 0xF3)
+#define SIT9531X_REG_HIZ_DIFF_811_MASK	SIT9531X_REG(0x03, 0xF4)
+#define SIT9531X_REG_HIZ_DIFF_811_STATE	SIT9531X_REG(0x03, 0xF5)
+#define SIT9531X_REG_HIZ_SE_07_MASK		SIT9531X_REG(0x03, 0xF8)
+#define SIT9531X_REG_HIZ_SE_07_STATE		SIT9531X_REG(0x03, 0xF9)
+#define SIT9531X_REG_HIZ_SE_811_MASK		SIT9531X_REG(0x03, 0xFA)
+#define SIT9531X_REG_HIZ_SE_811_STATE	SIT9531X_REG(0x03, 0xFB)
+
+/*
+ * Output divider registers in Pages 3/4.  Each output has a 34-bit
+ * integer divider mapped to 5 bytes (LSB at base reg, MSB at base-4).
+ * Outputs 0-5 are on Page 3, outputs 6-11 are on Page 4.
+ *
+ * The base register for slot N within a page is:
+ *   clkout_odr_divn_base[slot] = { 0x14, 0x24, 0x34, 0x44, 0x54, 0x64 }
+ *
+ * Layout: base=LSB, base-1, base-2, base-3, base-4[1:0]=MSB.
+ *
+ * Per-chip clkout_map[] translates output index to slot position.
+ */
+#define SIT9531X_PAGE_OUTSYS0_SLOT_MAX	5   /* slots 0-5 on Page 0x03 */
+
+/* Misc output system registers */
+#define SIT9531X_REG_PRG_DIR_GEN		SIT9531X_REG(0x03, 0x0F)
+#define SIT9531X_PRG_CMD_STATE		0x01
+#define SIT9531X_UPDATE_NVM			0x10
+#define SIT9531X_LOOP_LOCK			0x40
+
+/* Debug register (same offset, per-page) */
+#define SIT9531X_REG_OUTSYS_DEBUG		SIT9531X_REG(0x03, 0xBD)
+#define SIT9531X_DEBUG_UNLOCK_VAL		0xC3
+
+/*
+ * Per-output programmable phase delay: 34-bit coarse (in VCO clock
+ * cycles) plus a 3-bit fine field with fixed 30 ps steps.  Each output
+ * has a five-byte block PROG6..PROG2:
+ *
+ *   base + 0  PROG6  [7:5] OPSTG_VCASC_BUMP (preserve via RMW)
+ *                    [4:2] PRG_RST_FINE_DELAY[2:0]
+ *                    [1:0] PRG_RST_DELAY[33:32]
+ *   base + 1  PROG5  [7:0] PRG_RST_DELAY[31:24]
+ *   base + 2  PROG4  [7:0] PRG_RST_DELAY[23:16]
+ *   base + 3  PROG3  [7:0] PRG_RST_DELAY[15:8]
+ *   base + 4  PROG2  [7:0] PRG_RST_DELAY[7:0]
+ *
+ * Outputs 0-5 are on Page 3, outputs 6-11 on Page 4.  The block base
+ * within a page is 0x15 + 16 * (out_idx % 6).
+ */
+#define SIT9531X_OUT_PRG_DELAY_BASE		0x15
+#define SIT9531X_OUT_PRG_SLOT_STRIDE		0x10
+#define SIT9531X_OUT_PRG_OPSTG_MASK		0xE0	/* bits [7:5], preserve */
+#define SIT9531X_OUT_PRG_FINE_SHIFT		2
+#define SIT9531X_OUT_PRG_FINE_MASK		0x1C	/* bits [4:2] */
+#define SIT9531X_OUT_PRG_COARSE_HI_MASK		0x03	/* bits [1:0] */
+#define SIT9531X_OUT_PRG_FINE_STEP_PS		30
+#define SIT9531X_OUT_PRG_FINE_MAX		7	/* 3-bit field */
+#define SIT9531X_OUT_PRG_COARSE_BITS		34
+
+/*
+ * Per-output pulse-count control byte used in SYSREF / SYNCB modes.
+ * Slot N within a page sits at 0x1B + 16 * (slot % 6).  Same page
+ * mapping as PRG_RST_DELAY: slots 0-5 on Page 3, slots 6-11 on Page 4.
+ */
+#define SIT9531X_OUT_PROG0_BASE		0x1B
+
+/*
+ * On-demand phase-flush fired from a register rather than a GPIO pin.
+ * DIVO_PHASE_SEL_REG selects the in-register trigger source and
+ * DIVO_PHASE_TRIG flushes the output phase when pulsed high then low.
+ * The unrelated OEb trigger pair in bits [7:6] must be preserved.
+ */
+#define SIT9531X_REG_GPIO_FUNC_CTRL1	SIT9531X_REG(0x00, 0x65)
+#define SIT9531X_DIVO_PHASE_SEL_REG	BIT(5)
+#define SIT9531X_DIVO_PHASE_TRIG	BIT(4)
+
+/* ---- PLL page registers (apply to pages 0x0A-0x0D) ---- */
+#define SIT9531X_PLL_REG_SMALL_UPDATE	0x0F
+
+/* On-demand phase-flush enable (PLL page reg 0x3D bit 7) */
+#define SIT9531X_PLL_REG_PHFL_CTRL	0x3D
+#define SIT9531X_PLL_PHFL_ON_DEMAND_EN	BIT(7)
+
+/*
+ * Loop-filter coefficients on PLL_PAGE regs 0x10-0x15 (3 normal +
+ * 3 fast-lock) are GUI/NVM-generated by the timing configurator and must not be
+ * reprogrammed at runtime; the register map flags them as
+ * "GUI generated configuration should not change manually".
+ */
+
+#define SIT9531X_PLL_REG_OUT_MAP_HI		0x27
+#define SIT9531X_PLL_REG_OUT_MAP_LO		0x28
+#define SIT9531X_PLL_REG_STATUS		0x31
+#define SIT9531X_PLL_REG_NVM_UPDATE		0x3F
+
+/* DIVN registers (free-run divider readback) */
+#define SIT9531X_PLL_REG_DIVN_INT		0x30
+#define SIT9531X_PLL_REG_DIVN_NUM		0x32  /* 4 bytes (0x32-0x35) */
+#define SIT9531X_PLL_REG_DIVN_DEN		0x38  /* 4 bytes (0x38-0x3B) */
+
+/* DIVN2 registers (sync divider readback) */
+#define SIT9531X_PLL_REG_DIVN2_INT		0x3E  /* 5 bytes (0x3E-0x42) */
+#define SIT9531X_PLL_REG_DIVN2_FRAC_NUM	0x43  /* 4 bytes (0x43-0x46) */
+#define SIT9531X_PLL_REG_DIVN2_FRAC_DEN	0x49  /* 4 bytes (0x49-0x4C) */
+
+/* Debug register unlock */
+#define SIT9531X_PLL_REG_DEBUG		0xBD
+#define SIT9531X_PLL_DEBUG_UNLOCK		0xC3
+
+/*
+ * Signal pathway debug readback -- PLL page.  Dig_Sys_ReadCode selects
+ * which point of the pathway is tapped, Dig_Sys_WriteCode carries the
+ * modifiers for that read, Dig_Sys_read7..read0 hold the sampled bytes
+ * and the trigger register latches a sample.  The TDC phase
+ * measurement is one tap among several, reached through read code 69.
+ */
+#define SIT9531X_PLL_REG_DBG_READ_CODE	0xB3
+#define SIT9531X_PLL_REG_DBG_WRITE_CODE	0xB4
+#define SIT9531X_DBG_LOW_FREQ_CLK_BIT	BIT(7)
+#define SIT9531X_PLL_REG_DBG_DATA_0		0xB5  /* [7:0] */
+#define SIT9531X_PLL_REG_DBG_DATA_1		0xB6  /* [15:8] */
+#define SIT9531X_PLL_REG_DBG_DATA_2		0xB7  /* [23:16] */
+#define SIT9531X_PLL_REG_DBG_DATA_3		0xB8  /* [31:24] */
+#define SIT9531X_PLL_REG_DBG_DATA_4		0xB9  /* [39:32] + sign */
+#define SIT9531X_PLL_REG_DBG_DATA_5		0xBA  /* [47:40] */
+#define SIT9531X_PLL_REG_DBG_DATA_6		0xBB
+#define SIT9531X_PLL_REG_DBG_DATA_7		0xBC
+#define SIT9531X_PLL_REG_DBG_TRIGGER		0xD0  /* read to latch a sample */
+#define SIT9531X_DBG_DATA_BYTES		8
+
+/* Read code of the TDC phase tap, and the sign bit of its sample */
+#define SIT9531X_DBG_READ_CODE_TDC		69
+#define SIT9531X_TDC_SIGN_BIT		3
+
+/*
+ * Read codes of the running DIVN taps.  Unlike the configuration
+ * registers these report what the digital loop currently commands, so
+ * they carry the correction the loop applies to track its reference.
+ * The integer part and the numerator share one tap, the denominator
+ * has its own.
+ */
+#define SIT9531X_DBG_READ_CODE_DIVN		0x57
+#define SIT9531X_DBG_READ_CODE_DIVN_DEN	0x56
+#define SIT9531X_DIVN_RT_NUM_BITS		48
+#define SIT9531X_DIVN_RT_INT_HI_BIT		BIT(0)
+
+/*
+ * DIVN carried as fixed point, and the unit the DPLL ABI wants the
+ * fractional frequency offset in.  Equal in value, distinct in meaning.
+ */
+#define SIT9531X_DIVN_SCALE		1000000000000ULL
+#define SIT9531X_PPT_PER_UNIT		1000000000000ULL
+
+/*
+ * TDC phase-difference measurement setup (PLL page).  Per the vendor
+ * PhaseDifferenceReader reference procedure, a valid TDC reading needs
+ * the digital loop filter frozen (so the VCO does not track out the
+ * measured offset mid-sample) and, on PLLA (the 1PPS phase DPLL), the
+ * automatic phase/frequency-lock helpers held off.
+ */
+#define SIT9531X_PLL_REG_DLPF_FREEZE0	0x8A
+#define SIT9531X_PLL_REG_DLPF_FREEZE1	0x8B
+#define SIT9531X_PLL_DLPF_FREEZE_BIT	BIT(5)  /* freeze DLPF (hitless HO) */
+#define SIT9531X_PLL_REG_APL_CTRL		0x4D
+#define SIT9531X_PLL_APL_WAKEUP_DIS_BIT	BIT(7)  /* disable APL at wake-up */
+#define SIT9531X_PLL_APL_SWITCH_EN_BIT	BIT(5)  /* APL on switchover */
+#define SIT9531X_PLL_REG_AFL_CTRL		0x4E
+#define SIT9531X_PLL_AFL_WAKEUP_DIS_BIT	BIT(5)  /* disable AFL at wake-up */
+#define SIT9531X_PLL_CONFIG47_PHFL_BIT	BIT(7)  /* PHFL enable (reg 0x47) */
+#define SIT9531X_PLL_REG_ACTIVE		0x02
+#define SIT9531X_PLL_ACTIVE_BIT		BIT(0)  /* PLL reached active state */
+#define SIT9531X_PLL_REG_RESTART		0x05
+#define SIT9531X_PLL_RESTART_BIT		BIT(0)  /* restart the PLL */
+#define SIT9531X_PLL_REG_ZDB0		0x2B
+#define SIT9531X_PLL_REG_ZDB1		0x1E
+#define SIT9531X_PLL_ZDB_EN_BIT		BIT(4)  /* zero-delay buffer enabled */
+
+/* PLL EXT page INTSYNC configuration registers */
+#define SIT9531X_PLL_EXT_PAGE(_idx)		(SIT9531X_PAGE_PLLA_EXT + (_idx))
+
+/* PLL STATUS register bits */
+#define SIT9531X_PLL_STATUS_LOCK		BIT(0)
+#define SIT9531X_PLL_STATUS_OUTER_DIS	BIT(5)
+
+/* P-polarity status registers */
+#define SIT9531X_CLKMON_P_STATUS_01		SIT9531X_REG(0x06, 0x02)  /* inputs 0,1 */
+#define SIT9531X_CLKMON_P_NOTIF_01		SIT9531X_REG(0x06, 0x03)
+#define SIT9531X_CLKMON_P_STATUS_23		SIT9531X_REG(0x06, 0x06)  /* inputs 2,3 */
+#define SIT9531X_CLKMON_P_NOTIF_23		SIT9531X_REG(0x06, 0x07)
+
+/* N-polarity status registers */
+#define SIT9531X_CLKMON_N_STATUS_01		SIT9531X_REG(0x06, 0x92)  /* inputs 0,1 */
+#define SIT9531X_CLKMON_N_NOTIF_01		SIT9531X_REG(0x06, 0x93)
+#define SIT9531X_CLKMON_N_STATUS_23		SIT9531X_REG(0x06, 0x96)  /* inputs 2,3 */
+#define SIT9531X_CLKMON_N_NOTIF_23		SIT9531X_REG(0x06, 0x97)
+
+/* Per-input bit offsets within clock monitor nibble */
+#define SIT9531X_CLKMON_FREQ_FINE		0  /* bit 0 / bit 4 */
+#define SIT9531X_CLKMON_FREQ_COARSE		1  /* bit 1 / bit 5 */
+#define SIT9531X_CLKMON_CLK_LOSS		2  /* bit 2 / bit 6 */
+#define SIT9531X_CLKMON_CLK_LOSS_FD		3  /* bit 3 / bit 7 */
+
+/* ---- Debug / NVM unlock registers ---- */
+#define SIT9531X_REG_DBG_UNLOCK1		0x24
+#define SIT9531X_REG_DBG_UNLOCK2		0x25
+
+/* ---- Variant ID values (single byte read from SIT9531X_REG_VARIANT_ID) ---- */
+#define SIT9531X_VARIANT_ID_95317	0x17
+#define SIT9531X_VARIANT_ID_95316	0x31
+
+#endif /* _SIT9531X_REGS_H */
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [PATCH net-next v7 2/3] dt-bindings: dpll: add SiTime SiT95316 clock generator
  2026-08-15 22:19 ` [PATCH net-next v7 2/3] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
@ 2026-08-16 22:19   ` sashiko-bot
  0 siblings, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-08-16 22:19 UTC (permalink / raw)
  To: Ali Rouhi; +Cc: devicetree, conor+dt, robh

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] The DT binding example uses a VCO frequency that falls outside the hardware's documented valid bands.
--

--- Patch [2]: [PATCH net-next v7 2/3] dt-bindings: dpll: add SiTime SiT95316 clock generator ---
commit 410e36c99d7032772301b076a8bb386cf2186fe9
Author: Ali Rouhi <arouhi@sitime.com>

dt-bindings: dpll: add SiTime SiT95316 clock generator

This commit adds device tree binding documentation for the SiTime SiT95316
and SiT95317 DPLL clock generators.

> diff --git a/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml b/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
> new file mode 100644
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
[ ... ]
> +  sitime,pll-fvco:
> +    $ref: /schemas/types.yaml#/definitions/uint64-array
> +    minItems: 4
> +    maxItems: 4
> +    description:
> +      Per-PLL VCO frequency in Hz for PLLA, PLLB, PLLC, PLLD.  Neither
> +      standard frequency suffix fits.  The VCO bands are 4915.2 MHz to
> +      5898.24 MHz and 6875 MHz to 7812.5 MHz, so every valid value
[ ... ]
> +examples:
> +  - |
> +    #include <dt-bindings/gpio/gpio.h>
> +    #include <dt-bindings/interrupt-controller/irq.h>
> +
> +    i2c {
> +        #address-cells = <1>;
> +        #size-cells = <0>;
> +
> +        dpll@68 {
> +            compatible = "sitime,sit95316";
> +            reg = <0x68>;
> +            clocks = <&xo2>;
> +            clock-names = "xtal";
> +            reset-gpios = <&gpio 78 GPIO_ACTIVE_LOW>;
> +            interrupts = <12 IRQ_TYPE_LEVEL_LOW>;
> +            dpll-types = "eec", "eec", "eec", "eec";
> +            sitime,pll-fvco = /bits/ 64 <6251500000 0 0 0>;

[Severity: Low]
Does the example VCO frequency of 6251500000 (6251.5 MHz) fall into the
unsupported gap between the two valid operating bands?

The property description defines the valid bands as 4915.2 to 5898.24 MHz
and 6875 to 7812.5 MHz. Is there an additional supported frequency range
missing from the description, or should the example be updated to use a
value within the documented hardware limits?

> +            sitime,output-pll-map = <0 0 0 0 0 0 0 0 0 0 0 0>;
[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260815221919.64226-1-arouhi@sitime.com?part=2

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH net-next v7 3/3] dpll: add SiTime SiT9531x DPLL clock driver
  2026-08-15 22:19 ` [PATCH net-next v7 3/3] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
@ 2026-08-16 22:19   ` sashiko-bot
  0 siblings, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-08-16 22:19 UTC (permalink / raw)
  To: Ali Rouhi; +Cc: conor+dt, robh, devicetree

Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [High] Unconditional insertion of DISCONNECTED pins into the hardware priority table.
- [High] Disconnecting INTSYNC as the last source fails to stop it from being tracked.
- [Medium] INTSYNC destination state reporting ignores the hardware priority table.
- [Medium] The INTSYNC destination pin incorrectly accepts DPLL_PIN_STATE_CONNECTED without enforcing it.
- [Medium] Input and INTSYNC destination pin states report CONNECTED and hide SELECTABLE pins during free-run mode.
- [Low] Input pins falsely advertise phase adjustment capabilities by providing phase_adjust_get and configuring a phase_range.
- [Low] Undefined macro `kzalloc_obj` used for memory allocation.
--

--- Patch [3]: [PATCH net-next v7 3/3] dpll: add SiTime SiT9531x DPLL clock driver ---
commit 5e338b25e73b0731bfca9ec0093c0c9753e2503c
Author: Ali Rouhi <arouhi@sitime.com>

dpll: add SiTime SiT9531x DPLL clock driver

This commit adds a DPLL subsystem driver for the SiTime SiT95316 and
SiT95317 clock generators. It provides support for lock monitoring,
input priority management, frequency offset measurement, and output
phase adjustment using the device's I2C interface and NVM.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> new file mode 100644
> index 0000000000000..cd283922e7ffa
> --- /dev/null
> +++ b/drivers/dpll/sit9531x/core.c
[ ... ]
> @@ -782,31 +782,31 @@ int sit9531x_input_prio_set(struct sit9531x_dev *sitdev, u8 pll_idx,
>  int sit9531x_input_prio_remove(struct sit9531x_dev *sitdev, u8 pll_idx,
>  			       u8 input_idx)
>  {
>  	u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
>  	u8 kept[SIT9531X_PRIO_MAX_SLOTS];
>  	u8 slot, count = 0;
>  	bool found = false;
>  	int rc;
>  
>  	lockdep_assert_held(&sitdev->multiop_lock);
>  
>  	if (pll_idx >= SIT9531X_NUM_PLLS)
>  		return -EINVAL;
>  
>  	rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
>  	if (rc)
>  		return rc;
>  
>  	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
>  		if (srcs[slot] == input_idx)
>  			found = true;
>  		else
>  			kept[count++] = srcs[slot];
>  	}
>  
>  	if (!found || count == 0)
>  		return 0;

[Severity: High]
If userspace requests to disconnect the INTSYNC pin when it is the only
source in the priority table, the count becomes 0 and this function
aborts without modifying the table.

Since the INTSYNC net lacks a physical buffer that can be disabled, does
this leave the hardware actively tracking the INTSYNC reference when it was
commanded to disconnect?

[ ... ]
> @@ -2526,17 +2526,17 @@ void sit9531x_dev_stop(struct sit9531x_dev *sitdev)
>  }
>  
>  static struct sit9531x_dpll_pin *
>  sit9531x_dpll_pin_alloc(struct sit9531x_dpll *sitdpll,
>  			enum dpll_pin_direction dir, u8 id)
>  {
>  	struct sit9531x_dpll_pin *pin;
>  
>  	pin = kzalloc_obj(*pin, GFP_KERNEL);

[Severity: Low]
Does this macro exist in the kernel headers?

The use of kzalloc_obj appears to be an undefined identifier that will cause
a build failure here, and in a few other allocation sites throughout the
driver (such as sit9531x_dpll_alloc and sit9531x_pin_props_get).

> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> new file mode 100644
> index 0000000000000..e258beceffdd4
> --- /dev/null
> +++ b/drivers/dpll/sit9531x/dpll.c
[ ... ]
> @@ -258,35 +258,35 @@ sit9531x_dpll_input_pin_state_on_dpll_get(const struct dpll_pin *pin,
>  					  void *pin_priv,
>  					  const struct dpll_device *dpll,
>  					  void *dpll_priv,
>  					  enum dpll_pin_state *state,
>  					  struct netlink_ext_ack *extack)
>  {
>  	struct sit9531x_dpll_pin *dpin = pin_priv;
>  	struct sit9531x_dpll *sitdpll = dpll_priv;
>  	struct sit9531x_dev *sitdev = sitdpll->dev;
>  	const struct sit9531x_chan *chan;
>  	const struct sit9531x_ref *ref;
>  
>  	chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
>  	ref = sit9531x_ref_state_get(sitdev, dpin->id);
>  
>  	mutex_lock(&sitdev->multiop_lock);
>  
>  	/*
>  	 * Report CONNECTED only when the PLL is genuinely locked to this
>  	 * input.  chan->locked already excludes a PLL the configuration
>  	 * leaves unused, which would otherwise claim its default source as
>  	 * connected right after probe; the inner loop has to be locked as
>  	 * well before the reference can be called connected.
>  	 */
>  	if (chan->locked && !chan->inner_lol &&
>  	    chan->selected_ref == dpin->id)
>  		*state = DPLL_PIN_STATE_CONNECTED;
>  	/*
>  	 * Selectable means this DPLL may choose the input, so it has to be
>  	 * in this DPLL's priority table -- ref->enabled only says the
>  	 * receiver is powered, which is shared by every DPLL fed from the
>  	 * same lane.  Signal quality is reported through the pin's own
>  	 * attributes rather than by demoting the state, so a lane that is
>  	 * routed here but momentarily in LOS stays selectable.
>  	 */
>  	else if (!chan->mode && (ref->pll_mask & BIT(sitdpll->id)))
>  		*state = DPLL_PIN_STATE_SELECTABLE;

[Severity: Medium]
When the PLL is in free-run mode (outer loop disabled), the state getters
can still see chan->locked as true if the outer loss-of-lock bit is clear.

Without verifying !chan->mode here, won't this incorrectly report the pin
as connected while the PLL is actually freely running and ignoring the
reference?

Additionally, because the subsequent branch for the selectable state requires
!chan->mode, are other eligible pins erroneously reported as disconnected
instead of selectable during free-run mode?

[ ... ]
> @@ -433,26 +433,26 @@ static int
>  sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
>  				 const struct dpll_device *dpll, void *dpll_priv,
>  				 u32 prio, struct netlink_ext_ack *extack)
>  {
>  	struct sit9531x_dpll_pin *dpin = pin_priv;
>  	struct sit9531x_dpll *sitdpll = dpll_priv;
>  	struct sit9531x_dev *sitdev = sitdpll->dev;
>  	int rc;
>  
>  	if (dpin->dir != DPLL_PIN_DIRECTION_INPUT) {
>  		NL_SET_ERR_MSG(extack, "Priority applies only to input pins");
>  		return -EINVAL;
>  	}
>  
>  	if (prio >= SIT9531X_PRIO_MAX_SLOTS) {
>  		NL_SET_ERR_MSG(extack, "Priority out of range (0-10)");
>  		return -EINVAL;
>  	}
>  
>  	mutex_lock(&sitdev->multiop_lock);
>  	rc = sit9531x_input_prio_set(sitdev, sitdpll->id,
>  				     sit9531x_input_hw_src(dpin->id),
>  				     (u8)prio);
>  	mutex_unlock(&sitdev->multiop_lock);

[Severity: High]
This logic updates the priority table unconditionally.

If a pin is currently in the disconnected state, will inserting it into the
hardware priority table cause the PLL to start tracking it automatically if
higher-priority sources fail?

[ ... ]
> @@ -585,18 +585,18 @@ static int
>  sit9531x_dpll_input_pin_phase_offset_get(const struct dpll_pin *pin,
>  					 void *pin_priv,
>  					 const struct dpll_device *dpll,
>  					 void *dpll_priv, s64 *phase_offset,
>  					 struct netlink_ext_ack *extack)
>  {
>  [ ... ]
>  static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
>  	.direction_get		= sit9531x_dpll_input_pin_direction_get,
>  	.frequency_get		= sit9531x_dpll_input_pin_frequency_get,
>  	.state_on_dpll_get	= sit9531x_dpll_input_pin_state_on_dpll_get,
>  	.state_on_dpll_set	= sit9531x_dpll_input_pin_state_on_dpll_set,
>  	.prio_get		= sit9531x_dpll_input_pin_prio_get,
>  	.prio_set		= sit9531x_dpll_input_pin_prio_set,
>  	.phase_adjust_get	= sit9531x_dpll_input_pin_phase_adjust_get,
>  	.phase_offset_get	= sit9531x_dpll_input_pin_phase_offset_get,

[Severity: Low]
Is it intentional to advertise phase adjustment on input pins?

Phase adjustment appears to be an output-only feature in this driver, and
the lack of a phase_adjust_set callback means setting the adjustment
is not supported for inputs.

[ ... ]
> @@ -744,22 +744,22 @@ sit9531x_dpll_intsync_src_state_on_dpll_set(const struct dpll_pin *pin,
>  static int
>  sit9531x_dpll_intsync_dst_state_on_dpll_get(const struct dpll_pin *pin,
>  					    void *pin_priv,
>  					    const struct dpll_device *dpll,
>  					    void *dpll_priv,
>  					    enum dpll_pin_state *state,
>  					    struct netlink_ext_ack *extack)
>  {
>  	struct sit9531x_dpll *sitdpll = dpll_priv;
>  	struct sit9531x_dev *sitdev = sitdpll->dev;
>  	const struct sit9531x_chan *chan;
>  
>  	chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
>  
>  	mutex_lock(&sitdev->multiop_lock);
>  	if (sitdev->intsync_src == sitdpll->id)
>  		*state = DPLL_PIN_STATE_DISCONNECTED;
>  	else if (chan->locked && !chan->inner_lol &&
>  		 chan->selected_ref == SIT9531X_INTSYNC_PIN_ID)
>  		*state = DPLL_PIN_STATE_CONNECTED;
>  	else if (!chan->mode && sitdev->intsync_src >= 0)
>  		*state = DPLL_PIN_STATE_SELECTABLE;

[Severity: Medium]
This unconditionally reports the INTSYNC pin as selectable as long as
another PLL is driving INTSYNC and this PLL is in automatic mode.

If userspace previously set the INTSYNC pin to a disconnected state (removing
it from the priority table), shouldn't this reflect the disconnected state
rather than bypassing the priority table check?

>  	else
>  		*state = DPLL_PIN_STATE_DISCONNECTED;
>  	mutex_unlock(&sitdev->multiop_lock);
>  
>  	return 0;
>  }
>  
>  /*
>   * sit9531x_dpll_intsync_dst_state_on_dpll_set - lock a PLL to INTSYNC
>   *
>   *   CONNECTED/SELECTABLE -> add INTSYNC to this PLL's priority table
>   *   DISCONNECTED         -> drop INTSYNC from this PLL's priority table
>   *
>   * INTSYNC is an internal net with no physical receiver, so only the
>   * per-PLL priority table is touched; the source pin controls generation.
>   */
>  static int
>  sit9531x_dpll_intsync_dst_state_on_dpll_set(const struct dpll_pin *pin,
>  					    void *pin_priv,
>  					    const struct dpll_device *dpll,
>  					    void *dpll_priv,
>  					    enum dpll_pin_state state,
>  					    struct netlink_ext_ack *extack)
>  {
>  	struct sit9531x_dpll *sitdpll = dpll_priv;
>  	struct sit9531x_dev *sitdev = sitdpll->dev;
>  	u8 hw_src = sit9531x_input_hw_src(SIT9531X_INTSYNC_PIN_ID);
>  	int rc;
>  
>  	mutex_lock(&sitdev->multiop_lock);
>  
>  	switch (state) {
>  	case DPLL_PIN_STATE_DISCONNECTED:
>  		rc = sit9531x_input_prio_remove(sitdev, sitdpll->id, hw_src);
>  		break;
>  	case DPLL_PIN_STATE_SELECTABLE:
>  	case DPLL_PIN_STATE_CONNECTED:
>  		if (sitdev->intsync_src == sitdpll->id) {
>  			NL_SET_ERR_MSG(extack,
>  				       "PLL cannot lock to the INTSYNC it drives");
>  			rc = -EINVAL;
>  			break;
>  		}
>  		rc = sit9531x_input_prio_add(sitdev, sitdpll->id, hw_src);
>  		break;

[Severity: Medium]
Is it correct to handle DPLL_PIN_STATE_CONNECTED identically to
DPLL_PIN_STATE_SELECTABLE here?

For physical inputs, the driver explicitly rejects connected states because
the device selects references by priority. Accepting it here silently leaves
the hardware in automatic selection mode, mismatched with the userspace
request for an exclusive connection.

> diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
> new file mode 100644
> index 0000000000000..8a0105c8c6472
> --- /dev/null
> +++ b/drivers/dpll/sit9531x/prop.c
[ ... ]
> @@ -245,15 +245,15 @@ sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
>  		props->dpll_props.type = DPLL_PIN_TYPE_GNSS;
>  		props->dpll_props.capabilities =
>  			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
>  		curr_freq = sitdev->out[index].freq;
>  	}
>  
>  	/* Allow phase-adjust over +/-1 ms window.  The subsystem rejects
>  	 * pin_set(phase-adjust, X) when X falls outside [min, max], so
>  	 * leaving these at 0 silently blocks every netlink call.  1 ms is
>  	 * well beyond the DCO dynamic range but costs nothing.
>  	 */
>  	props->dpll_props.phase_range.min = -1000000000; /* -1 ms in ps */
>  	props->dpll_props.phase_range.max =  1000000000; /* +1 ms in ps */

[Severity: Low]
This unconditionally configures phase range limits for all pins, including
inputs. Since inputs do not support phase adjustment, should these limits
be restricted to output pins?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260815221919.64226-1-arouhi@sitime.com?part=3

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-08-16 22:19 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-15 22:19 [PATCH net-next v7 0/3] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
2026-08-15 22:19 ` [PATCH net-next v7 1/3] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
2026-08-15 22:19 ` [PATCH net-next v7 2/3] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
2026-08-16 22:19   ` sashiko-bot
2026-08-15 22:19 ` [PATCH net-next v7 3/3] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
2026-08-16 22:19   ` sashiko-bot

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.