mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH net-next v9 01/15] dt-bindings: vendor-prefixes: add SiTime Corporation
  2026-09-15  0:00 [PATCH net-next v9 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
@ 2026-09-15  0:00 ` Ali Rouhi
  2026-09-15  0:00 ` [PATCH net-next v9 03/15] dpll: add basic SiTime SiT9531x support Ali Rouhi
                   ` (13 subsequent siblings)
  14 siblings, 0 replies; 33+ messages in thread
From: Ali Rouhi @ 2026-09-15  0:00 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel, 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 ba2002969373..65da27d6e84d 100644
--- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
+++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
@@ -1563,6 +1563,8 @@ patternProperties:
     description: SiRF Technology, Inc.
   "^sis,.*":
     description: Silicon Integrated Systems Corp.
+  "^sitime,.*":
+    description: SiTime Corporation
   "^sitronix,.*":
     description: Sitronix Technology Corporation
   "^skov,.*":
-- 
2.39.2 (Apple Git-143)


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

* [PATCH net-next v9 00/15] dpll: add SiTime SiT9531x DPLL clock driver
@ 2026-09-15  0:00 Ali Rouhi
  2026-09-15  0:00 ` [PATCH net-next v9 01/15] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
                   ` (14 more replies)
  0 siblings, 15 replies; 33+ messages in thread
From: Ali Rouhi @ 2026-09-15  0:00 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel, 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 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/
v7 https://lore.kernel.org/netdev/20260815221919.64226-1-arouhi@sitime.com/
v8 https://lore.kernel.org/netdev/20260902214030.20955-1-arouhi@sitime.com/

The driver was a single 5.6k-line patch through v7. Vadim and Ivan asked
for it to be split into logical parts, with the first zl3073x series as
the model, so v8 introduced it feature by feature and v9 keeps that
shape:

  1-2   bindings: vendor prefix, then the device schema
  3     basic support: paged regmap, variant detection, probe
  4     DPLL types and pin properties from system firmware
  5     register the DPLL devices and pins, and keep their state
  6     input pin state on a DPLL, with the pin-state contract
  7     input pin priority
  8     pin frequency, both directions
  9     output pin state (mute)
  10    output phase adjust
  11    esync on outputs
  12    phase offset through the TDC
  13    fractional frequency offset of the selected reference
  14    the inter-PLL sync net as a pair of pins
  15    optional device-tree overrides for two board facts

The two bindings patches come first, so the driver never matches on a
compatible string before the schema that describes it is in the tree.

Each of the thirteen driver patches builds and links on its own: no
patch calls something a later patch introduces, so a bisect cannot land
on a tree that fails to compile. For this posting that was re-checked
patch by patch with W=1 and with sparse. checkpatch --strict is clean
except for the "does MAINTAINERS need updating?" hint on patches 4 and
5, which add files under drivers/dpll/sit9531x/ -- patch 3 already
covers that directory with an F: entry.

On process, and on Paolo's note against v8: the v8 AI review raised 86
comments across the series, and each one now has an individual reply in
its own sub-thread -- fixed, or answered with the reason it is not a
bug. The second half of that guidance is the more useful half, and we
had not been following it. We now run the same review locally before
posting rather than after; v9 has been through it twice over the whole
series, and what it found is folded into the patches below instead of
being left for the bots to raise again. We intend to keep doing this for
any further revision.

Changes in v9:

  - Put the cost and the constraints in the commit messages, next to the
    code they apply to, rather than only in replies on the list. A
    priority-table rewrite forces holdover and takes ten to twenty
    milliseconds, because the device cannot change one slot in
    isolation. Programming an output divider costs about a hundred
    milliseconds under the DPLL core's global lock, most of it the
    settling time the part requires after the loop-lock command. The
    phase flush that follows a divider write realigns every output on
    that PLL, not only the one that changed. The clock_id carries the
    I2C adapter number in eight bits, so the collision it admits is
    stated rather than designed around. The debug window returns the
    previous latch, which is why every read of it triggers three times.
    The running feedback divider is two taps sampled separately and can
    tear; on a locked loop it moves below what the measurement resolves,
    and the part offers no way to latch both at once.

  - Never leave the device armed when a sequence fails partway. The
    programming state, the debug window, the on-demand phase flush and
    forced holdover are each entered by one write and left by another,
    and every fallible step in between now exits through the common
    path that issues the second write. The first error is the one
    returned, so a transient I2C failure can no longer leave the chip
    with its loops open, its debug block unlocked, its flush armed or a
    PLL parked in holdover.

  - Answer from the device rather than from a cache where the cache can
    be wrong. Whether an output is muted now reads both the force bit
    and the state bit, and picks the register bank from the pad's own
    CMOS enables, so a single-ended pad is no longer interrogated
    through the differential bank or the reverse. State that a write may
    have invalidated is marked stale and re-read on the next get instead
    of being reported from the last poll. Priority-table membership is
    asked of the table.

  - Make .mode_set() idempotent. It reads the outer-loop disable bit
    before writing it and does nothing when the PLL is already in
    automatic mode. Without that, setting the only mode the driver
    advertises a second time, followed by one failed latch, would
    restore a disable the PLL never had and drop a running loop into
    free-run.

  - Park the poll worker and the INTRB interrupt across system suspend
    and take a fresh sample on resume. The tick talks to the device over
    I2C, and one landing mid-suspend could tear a paged sequence between
    the page-selector write and the register access.

  - Check a PLL's VCO frequency against the device's two bands before
    using it, so a value that fell in the gap between them cannot reach
    the divider arithmetic.

  - Binding: accept clock-frequency as an alternative to the clocks
    phandle, for platforms whose firmware does not expose the crystal
    through the clock framework; exactly one of the two is now required.
    The sitime,pll-fvco description was also rewritten to say why the
    property carries no unit suffix: the values exceed 32 bits and so
    need an explicit uint64-array type, and the dtschema meta-schema
    does not permit a type $ref on a "-hz" property. That was a review
    suggestion we tried and dt_binding_check rejected, so the reason is
    recorded in the binding itself. The schema changed, so Krzysztof's
    Reviewed-by is not carried across it. Patch 1 is unchanged since v8
    and keeps Conor's Acked-by.

One finding from the v8 review is declined rather than fixed, and the
reply on patch 8 gives the argument: a frequency request of 0 Hz is
refused with -EINVAL instead of being treated as a request to stop the
output. Nothing in the ABI says zero means off, and this device already
has a mute control that says so explicitly.

The use-after-free report on the pin properties remains 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.

The review also turned up one bug that is not ours: dpll_pin_freq_set()
reads the requested frequency as a u64 and validates it through a helper
that takes a u32, so a rate of U32_MAX + 1 + N is accepted as N against
the supported ranges, which are themselves u64. That affects every
driver behind the interface, not just this one. It is a separate patch
against the core and will be posted on its own rather than buried in a
driver series; this driver range-checks its own input in the meantime.

Ali Rouhi (2):
  dt-bindings: vendor-prefixes: add SiTime Corporation
  dt-bindings: dpll: add SiTime SiT95316 clock generator

Oleg Zadorozhnyi (13):
  dpll: add basic SiTime SiT9531x support
  dpll: sit9531x: read DPLL types and pin properties from system
    firmware
  dpll: sit9531x: register DPLL devices and pins
  dpll: sit9531x: implement input pin state on a DPLL
  dpll: sit9531x: add support to get and set priority on input pins
  dpll: sit9531x: add support to get and set frequency on pins
  dpll: sit9531x: implement output pin state on a DPLL
  dpll: sit9531x: add support to adjust output phase
  dpll: sit9531x: add support to get and set esync on pins
  dpll: sit9531x: add support to get phase offset on the connected input
    pin
  dpll: sit9531x: add support to get fractional frequency offset
  dpll: sit9531x: model the inter-PLL sync net as a pair of pins
  dpll: sit9531x: allow the device tree to override two board facts

 .../bindings/dpll/sitime,sit95316.yaml        |  181 +
 .../devicetree/bindings/vendor-prefixes.yaml  |    2 +
 MAINTAINERS                                   |    7 +
 drivers/dpll/Kconfig                          |    2 +
 drivers/dpll/Makefile                         |    1 +
 drivers/dpll/sit9531x/Kconfig                 |   17 +
 drivers/dpll/sit9531x/Makefile                |    4 +
 drivers/dpll/sit9531x/core.c                  | 4130 +++++++++++++++++
 drivers/dpll/sit9531x/core.h                  |  407 ++
 drivers/dpll/sit9531x/dpll.c                  | 1486 ++++++
 drivers/dpll/sit9531x/dpll.h                  |   70 +
 drivers/dpll/sit9531x/prop.c                  |  437 ++
 drivers/dpll/sit9531x/prop.h                  |   39 +
 drivers/dpll/sit9531x/regs.h                  |  394 ++
 14 files changed, 7177 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: d0ec95a8a4e79f2fd6063fc8932415db8c227689
-- 
2.39.2 (Apple Git-143)


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

* [PATCH net-next v9 02/15] dt-bindings: dpll: add SiTime SiT95316 clock generator
  2026-09-15  0:00 [PATCH net-next v9 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
  2026-09-15  0:00 ` [PATCH net-next v9 01/15] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
  2026-09-15  0:00 ` [PATCH net-next v9 03/15] dpll: add basic SiTime SiT9531x support Ali Rouhi
@ 2026-09-15  0:00 ` Ali Rouhi
  2026-09-17 15:01   ` netdev-bot+sashiko
  2026-09-15  0:00 ` [PATCH net-next v9 05/15] dpll: sit9531x: register DPLL devices and pins Ali Rouhi
                   ` (11 subsequent siblings)
  14 siblings, 1 reply; 33+ messages in thread
From: Ali Rouhi @ 2026-09-15  0:00 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel, Ali Rouhi

Add a binding for the SiTime SiT9531x family of clock generators: an
I2C-controlled device with four independent PLLs, up to eight input
clocks and up to twelve outputs, described as a DPLL provider.

Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 .../bindings/dpll/sitime,sit95316.yaml        | 181 ++++++++++++++++++
 MAINTAINERS                                   |   6 +
 2 files changed, 187 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..e71ee3611a83
--- /dev/null
+++ b/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
@@ -0,0 +1,181 @@
+# 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
+
+  clock-frequency:
+    description:
+      XO rate in Hz feeding XIN/XO_CLK.  Alternative to the "clocks"
+      phandle for platforms where the firmware does not expose the
+      oscillator through the clock framework.
+
+  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.  The
+      values exceed 32 bits, which requires an explicit uint64-array
+      type; the dtschema meta-schema does not permit a type $ref on a
+      "-hz" unit-suffix property (only opp-hz is defined as 64-bit),
+      hence no unit suffix.  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
+
+oneOf:
+  - required:
+      - clocks
+      - clock-names
+  - required:
+      - clock-frequency
+
+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 <6900000000 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>;
+                };
+            };
+        };
+    };
+...
diff --git a/MAINTAINERS b/MAINTAINERS
index 0b42e898f4d8..ef07a53b5fc4 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25157,6 +25157,12 @@ 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
+
 SL28 CPLD MFD DRIVER
 M:	Michael Walle <mwalle@kernel.org>
 S:	Maintained
-- 
2.39.2 (Apple Git-143)


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

* [PATCH net-next v9 03/15] dpll: add basic SiTime SiT9531x support
  2026-09-15  0:00 [PATCH net-next v9 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
  2026-09-15  0:00 ` [PATCH net-next v9 01/15] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
@ 2026-09-15  0:00 ` Ali Rouhi
  2026-09-17  8:42   ` Ivan Vecera
  2026-09-17 15:01   ` netdev-bot+sashiko
  2026-09-15  0:00 ` [PATCH net-next v9 02/15] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
                   ` (12 subsequent siblings)
  14 siblings, 2 replies; 33+ messages in thread
From: Ali Rouhi @ 2026-09-15  0:00 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel, Ali Rouhi

From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>

The SiT9531x is an I2C clock generator with four independent PLLs, up to
eight input clocks and up to twelve outputs.  Later patches register the
PLLs with the DPLL subsystem and expose the clocks as pins; this one adds
only what is needed to reach the device.

The register space is paged: 32 pages of 256 registers, selected by
writing the page number to offset 0xFF, which is present in every page.
A regmap range configuration describes that window, so the rest of the
driver addresses a register as a page and an offset and never touches the
selector itself.  Pages come in pairs for the PLLs (0x0A/0x1A for PLLA,
and so on).

Probe reads the rate of the crystal feeding XIN, since every frequency
the driver later computes derives from it; takes the optional reset line
and leaves it deasserted, because the device configuration comes from
efuse or from a blob applied before probe and a reset would discard it;
then identifies the variant from the single byte at page 0 offset 0x02
and refuses to bind on anything unknown.

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                    |   1 +
 drivers/dpll/Kconfig           |   2 +
 drivers/dpll/Makefile          |   1 +
 drivers/dpll/sit9531x/Kconfig  |  17 +++
 drivers/dpll/sit9531x/Makefile |   4 +
 drivers/dpll/sit9531x/core.c   | 272 +++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/core.h   |  91 +++++++++++
 drivers/dpll/sit9531x/regs.h   |  56 +++++++
 8 files changed, 444 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/regs.h

diff --git a/MAINTAINERS b/MAINTAINERS
index ef07a53b5fc4..73cc0634c213 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25162,6 +25162,7 @@ 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>
diff --git a/drivers/dpll/Kconfig b/drivers/dpll/Kconfig
index be98969f040a..f8f7ca121b93 100644
--- a/drivers/dpll/Kconfig
+++ b/drivers/dpll/Kconfig
@@ -23,6 +23,8 @@ 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..a221fe55386a
--- /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
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
new file mode 100644
index 000000000000..b0920f76362a
--- /dev/null
+++ b/drivers/dpll/sit9531x/core.c
@@ -0,0 +1,272 @@
+// 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>
+ *
+ * I2C probe, paged regmap configuration and register access helpers.
+ */
+
+#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/kernel.h>
+#include <linux/module.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+
+#include "core.h"
+#include "regs.h"
+
+#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);
+}
+
+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;
+}
+
+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 derived from the XO
+	 * feeding XIN/XO_CLK, so the rate is needed before anything can be
+	 * computed from a divider.  The rate normally comes from a "clocks"
+	 * phandle (clock-names = "xtal").  On platforms where the firmware
+	 * does not expose the XO through the clock framework, fall back to
+	 * a "clock-frequency" device property.
+	 */
+	xtal_clk = devm_clk_get_optional_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) {
+		u32 freq;
+
+		if (!device_property_read_u32(sitdev->dev, "clock-frequency",
+					      &freq))
+			sitdev->xtal_freq = freq;
+	}
+	if (!sitdev->xtal_freq)
+		return dev_err_probe(sitdev->dev, -EINVAL,
+				     "no xtal rate: provide clocks=<&xo> + clock-names=\"xtal\", or a clock-frequency property\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 */
+
+	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);
+
+	rc = devm_mutex_init(sitdev->dev, &sitdev->multiop_lock);
+	if (rc)
+		return dev_err_probe(sitdev->dev, rc,
+				     "Failed to initialize mutex\n");
+
+	dev_info(sitdev->dev, "%s detected, %u inputs, %u outputs\n",
+		 sitdev->info->name, sitdev->info->num_inputs,
+		 sitdev->info->num_outputs);
+
+	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..76a2632f0ce4
--- /dev/null
+++ b/drivers/dpll/sit9531x/core.h
@@ -0,0 +1,91 @@
+/* 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/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_MAX_OUTPUTS		12
+
+/*
+ * 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;
+};
+
+/*
+ * struct sit9531x_dev - SiT9531x device instance
+ * @dev:		parent device
+ * @client:		I2C client
+ * @regmap:		paged register map
+ * @info:		detected chip variant info
+ * @multiop_lock:	serializes multi-register sequences
+ * @xtal_freq:		crystal oscillator frequency in Hz
+ * @reset_gpio:		optional reset line (DT "reset-gpios"), NULL if absent
+ */
+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;
+
+	u32			xtal_freq;
+
+	struct gpio_desc	*reset_gpio;
+};
+
+/*
+ * 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;
+}
+
+extern const struct regmap_config sit9531x_regmap_config;
+
+/* ---- Core lifecycle ---- */
+int  sit9531x_dev_probe(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);
+
+#endif /* _SIT9531X_CORE_H */
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
new file mode 100644
index 000000000000..ae553bec0fd8
--- /dev/null
+++ b/drivers/dpll/sit9531x/regs.h
@@ -0,0 +1,56 @@
+/* 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 0xFF, present in every page
+ *   - Each page has 256 registers (0x00-0xFF)
+ *   - 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
+
+/*
+ * 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)
+
+/* Variant ID values (page 0 reg 0x02) */
+#define SIT9531X_VARIANT_ID_95317	0x17
+#define SIT9531X_VARIANT_ID_95316	0x31
+
+#endif /* _SIT9531X_REGS_H */
-- 
2.39.2 (Apple Git-143)


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

* [PATCH net-next v9 04/15] dpll: sit9531x: read DPLL types and pin properties from system firmware
  2026-09-15  0:00 [PATCH net-next v9 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (3 preceding siblings ...)
  2026-09-15  0:00 ` [PATCH net-next v9 05/15] dpll: sit9531x: register DPLL devices and pins Ali Rouhi
@ 2026-09-15  0:00 ` Ali Rouhi
  2026-09-17  9:42   ` Ivan Vecera
  2026-09-17 15:01   ` netdev-bot+sashiko
  2026-09-15  0:00 ` [PATCH net-next v9 06/15] dpll: sit9531x: implement input pin state on a DPLL Ali Rouhi
                   ` (9 subsequent siblings)
  14 siblings, 2 replies; 33+ messages in thread
From: Ali Rouhi @ 2026-09-15  0:00 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel, Ali Rouhi

From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>

The DPLL core wants a type per device and a property set per pin: a
label, the direction, the capabilities, and the frequencies the pin
supports.  None of that can be read from the chip -- which frequencies a
board actually presents on a given input, and whether an output's
embedded sync is wired to be controllable, are board facts -- so they
come from the firmware node, with defaults for a node that does not
describe them.

Input pins are interleaved P and N lanes of four differential pairs, so a
logical index maps to a pair and a lane, and a pair configured
single-ended presents two independent inputs where a differential one
presents one.  The labels follow from that, and the two extra input
positions -- the crystal and the inter-PLL sync net -- are named
separately.

Kept in its own file, and introduced before anything is registered, so
the registration code that follows has nothing to say about firmware.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/Makefile |   2 +-
 drivers/dpll/sit9531x/core.h   |  72 ++++++
 drivers/dpll/sit9531x/prop.c   | 437 +++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/prop.h   |  39 +++
 4 files changed, 549 insertions(+), 1 deletion(-)
 create mode 100644 drivers/dpll/sit9531x/prop.c
 create mode 100644 drivers/dpll/sit9531x/prop.h

diff --git a/drivers/dpll/sit9531x/Makefile b/drivers/dpll/sit9531x/Makefile
index a221fe55386a..819af61123f5 100644
--- a/drivers/dpll/sit9531x/Makefile
+++ b/drivers/dpll/sit9531x/Makefile
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 
 obj-$(CONFIG_SIT9531X_DPLL) += sit9531x.o
-sit9531x-y := core.o
+sit9531x-y := core.o prop.o
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 76a2632f0ce4..230b21b9e238 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -23,7 +23,17 @@
 
 #define SIT9531X_NUM_PLLS		4
 #define SIT9531X_MAX_INPUTS		8
+#define SIT9531X_NUM_INPUT_PAIRS	(SIT9531X_MAX_INPUTS / 2)
 #define SIT9531X_MAX_OUTPUTS		12
+/*
+ * INTSYNC (the inter-PLL sync net) is modeled 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
 
 /*
  * struct sit9531x_chip_info - chip variant identification
@@ -41,6 +51,39 @@ struct sit9531x_chip_info {
 	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
+ * @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;
+	const char			*label;
+	enum sit9531x_signal_mode	sig_mode;
+};
+
+/*
+ * struct sit9531x_out - output state
+ * @freq:		configured frequency in Hz
+ * @label:		board label from DT or default
+ */
+struct sit9531x_out {
+	u32		freq;
+	const char	*label;
+};
+
 /*
  * struct sit9531x_dev - SiT9531x device instance
  * @dev:		parent device
@@ -48,6 +91,8 @@ struct sit9531x_chip_info {
  * @regmap:		paged register map
  * @info:		detected chip variant info
  * @multiop_lock:	serializes multi-register sequences
+ * @ref:		array of input reference states
+ * @out:		array of output states
  * @xtal_freq:		crystal oscillator frequency in Hz
  * @reset_gpio:		optional reset line (DT "reset-gpios"), NULL if absent
  */
@@ -59,11 +104,38 @@ struct sit9531x_dev {
 	/* 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];
 	u32			xtal_freq;
 
 	struct gpio_desc	*reset_gpio;
 };
 
+/*
+ * 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_pll_page - get register page for PLL index
  * @pll_idx: PLL index (0 = PLLA, 3 = PLLD)
diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
new file mode 100644
index 000000000000..c9642b702c4a
--- /dev/null
+++ b/drivers/dpll/sit9531x/prop.c
@@ -0,0 +1,437 @@
+// 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;
+	u64 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 {
+		/*
+		 * A synthesized clock output is an external connection with
+		 * no more specific meaning; a board that knows better says
+		 * so through the pin's connection-type property below.
+		 */
+		props->dpll_props.type = DPLL_PIN_TYPE_EXT;
+		props->dpll_props.capabilities =
+			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+		curr_freq = sitdev->out[index].freq;
+
+		/*
+		 * Allow phase-adjust over a +/-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.  Only outputs get a range: input pins
+		 * have no .phase_adjust_set, and advertising one there would
+		 * promise userspace something every set would refuse.
+		 */
+		props->dpll_props.phase_range.min = -1000000000; /* -1 ms in ps */
+		props->dpll_props.phase_range.max =  1000000000; /* +1 ms in ps */
+		/*
+		 * The fine step is 30 ps, but requests are accepted at 1 ps
+		 * resolution and rounded to the nearest achievable delay, so
+		 * advertise the request granularity, not the hardware step.
+		 */
+		props->dpll_props.phase_gran = 1;
+	}
+
+	/* 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 ||
+	     index == SIT9531X_MAX_INPUTS))
+		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"))
+			props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+		else if (!strcmp(type, "synce"))
+			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;
+	}
+
+	rc = fwnode_property_read_u64_array(props->fwnode,
+					    "supported-frequencies-hz",
+					    freqs, num_freqs);
+	if (rc) {
+		dev_warn(sitdev->dev,
+			 "failed to parse supported-frequencies-hz for %s: %d\n",
+			 props->package_label, rc);
+		goto err_alloc_ranges;
+	}
+
+	/*
+	 * 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 ||
+		    index != SIT9531X_MAX_INPUTS)
+			curr_freq = 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;
+
+	/*
+	 * Advertise only concrete values from firmware plus current runtime
+	 * value. For outputs without a firmware list, publish one wide range as
+	 * an explicit fallback because those pins do support frequency_set.
+	 */
+	ranges = kcalloc(num_freqs + 2, sizeof(*ranges), GFP_KERNEL);
+	if (!ranges) {
+		rc = -ENOMEM;
+		goto err_alloc_ranges;
+	}
+
+	/*
+	 * Publish the seeded rate only once the pin is certain to be built.
+	 * The allocation above is the last thing that can fail, and a call
+	 * that reports failure must not leave the device's cached rate
+	 * changed behind it.
+	 */
+	if (curr_freq) {
+		if (dir == DPLL_PIN_DIRECTION_INPUT)
+			sitdev->ref[index].freq = curr_freq;
+		else
+			sitdev->out[index].freq = curr_freq;
+	}
+
+	j = 0;
+
+	/* Current frequency first, when known. */
+	if (curr_freq)
+		ranges[j++] = (struct dpll_pin_frequency)DPLL_PIN_FREQUENCY(curr_freq);
+
+	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;
+	}
+
+	if (dir == DPLL_PIN_DIRECTION_OUTPUT && num_freqs == 0) {
+		ranges[j].min = 1;
+		ranges[j].max = 1000000000ULL; /* 1 GHz */
+		j++;
+	}
+
+	if (j > 0) {
+		props->dpll_props.freq_supported = ranges;
+		props->dpll_props.freq_supported_num = j;
+	} else {
+		kfree(ranges);
+		props->dpll_props.freq_supported = NULL;
+		props->dpll_props.freq_supported_num = 0;
+	}
+
+	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 */
-- 
2.39.2 (Apple Git-143)


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

* [PATCH net-next v9 05/15] dpll: sit9531x: register DPLL devices and pins
  2026-09-15  0:00 [PATCH net-next v9 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (2 preceding siblings ...)
  2026-09-15  0:00 ` [PATCH net-next v9 02/15] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
@ 2026-09-15  0:00 ` Ali Rouhi
  2026-09-17 15:01   ` netdev-bot+sashiko
  2026-09-15  0:00 ` [PATCH net-next v9 04/15] dpll: sit9531x: read DPLL types and pin properties from system firmware Ali Rouhi
                   ` (10 subsequent siblings)
  14 siblings, 1 reply; 33+ messages in thread
From: Ali Rouhi @ 2026-09-15  0:00 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel, Ali Rouhi

From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>

Register each of the four on-chip PLLs as a DPLL device and the clocks
around them as pins: the inputs, the crystal, and the outputs the loaded
configuration routes to a PLL.  An output routed to no PLL gets no pin,
since there would be nothing for the core to report about it.

The callbacks a device needs from the start come with it -- lock status
and the operating mode -- along with the direction of each pin.  The rest
of the pin operations follow in later patches.

Both need somewhere to read from.  The chip reports its state across
several pages, and a netlink call must not turn into a burst of I2C
transactions, so a kthread worker polls the interesting registers twice a
second into a per-input, per-output and per-PLL cache, and the callbacks
answer from it.  The same tick compares the new state against the old and
notifies the core only on a change.  Where a board wires INTRB, the
interrupt schedules that tick immediately rather than replacing it: the
poll stays as the fallback, since this device gives no way to be sure an
edge was not missed.

The clock_id that keys a device in the DPLL core is built from the chip's
variant and its position on the bus, with the adapter number carried in
eight bits.  Two identical parts at the same address on adapters whose
numbers differ by a multiple of 256 would therefore collide.  That needs a
machine with more than 256 I2C adapters and the same part twice at the
same address on two of them; the alternative is a wider identifier that no
other driver expects, so the constraint is stated rather than designed
around.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/Makefile |    2 +-
 drivers/dpll/sit9531x/core.c   | 1641 ++++++++++++++++++++++++++++++--
 drivers/dpll/sit9531x/core.h   |  268 +++++-
 drivers/dpll/sit9531x/dpll.c   |  444 +++++++++
 drivers/dpll/sit9531x/dpll.h   |   69 ++
 drivers/dpll/sit9531x/regs.h   |  232 ++++-
 6 files changed, 2554 insertions(+), 102 deletions(-)
 create mode 100644 drivers/dpll/sit9531x/dpll.c
 create mode 100644 drivers/dpll/sit9531x/dpll.h

diff --git a/drivers/dpll/sit9531x/Makefile b/drivers/dpll/sit9531x/Makefile
index 819af61123f5..b97d2656a460 100644
--- a/drivers/dpll/sit9531x/Makefile
+++ b/drivers/dpll/sit9531x/Makefile
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 
 obj-$(CONFIG_SIT9531X_DPLL) += sit9531x.o
-sit9531x-y := core.o prop.o
+sit9531x-y := core.o dpll.o prop.o
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index b0920f76362a..6dfb1b31d491 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -6,9 +6,11 @@
  * Author: Ali Rouhi <arouhi@sitime.com>
  * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
  *
- * I2C probe, paged regmap configuration and register access helpers.
+ * 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>
@@ -16,14 +18,26 @@
 #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/pm.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) }
@@ -145,122 +159,1597 @@ int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
 	return regmap_update_bits(sitdev->regmap, reg, mask, val);
 }
 
-static int sit9531x_read_variant_id(struct sit9531x_dev *sitdev, u8 *id)
+/*
+ * 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)
 {
-	return sit9531x_read_u8(sitdev, SIT9531X_REG_VARIANT_ID, id);
+	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;
+	}
 }
 
-static const struct sit9531x_chip_info *sit9531x_match_variant(u8 id)
-{
-	unsigned int i;
+/*
+ * 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[].
+ */
 
-	for (i = 0; i < ARRAY_SIZE(sit9531x_chip_ids); i++) {
-		if (sit9531x_chip_ids[i].id == id)
-			return &sit9531x_chip_ids[i];
+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;
 	}
+}
 
-	return NULL;
+/*
+ * sit9531x_output_mode_fetch - read how an output is wired
+ *
+ * The Hi-Z force is a separate register pair for the differential and the
+ * single-ended path, and only the pair belonging to the way the output is
+ * actually wired says anything about whether it is quiet.  The other pair
+ * holds whatever the loaded configuration left in it.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_output_mode_fetch(struct sit9531x_dev *sitdev, u8 out_idx)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	u8 slot, page, reg, val;
+	int rc;
+
+	slot = info->clkout_map[out_idx];
+	page = (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX) ?
+		SIT9531X_PAGE_OUTSYS1 : SIT9531X_PAGE_OUTSYS0;
+	reg = SIT9531X_OUT_MISC0_BASE +
+	      SIT9531X_OUT_MISC0_STRIDE * (slot % 6);
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG(page, reg), &val);
+	if (rc)
+		return rc;
+
+	sitdev->out[out_idx].cmos = !!(val & (SIT9531X_OUT_CMOS_ENP |
+					      SIT9531X_OUT_CMOS_ENN));
+
+	return 0;
 }
 
-int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
+/*
+ * 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 out_idx, bool *muted)
 {
-	struct clk *xtal_clk;
-	u8 variant_id;
+	struct sit9531x_hiz_regs r;
+	unsigned int mask_reg, state_reg;
+	u8 mask, state;
 	int rc;
 
+	sit9531x_output_get_hiz_regs(sitdev->info->clkout_map[out_idx], &r);
+
 	/*
-	 * Fvco = Fref * (DIVN + frac/2^32) with Fref derived from the XO
-	 * feeding XIN/XO_CLK, so the rate is needed before anything can be
-	 * computed from a divider.  The rate normally comes from a "clocks"
-	 * phandle (clock-names = "xtal").  On platforms where the firmware
-	 * does not expose the XO through the clock framework, fall back to
-	 * a "clock-frequency" device property.
+	 * Read the pair that belongs to the way this output is wired.
+	 * Testing both and taking either as proof of a mute answers
+	 * from a register nothing drives, and disagrees with itself
+	 * when a mute lands on one pair and fails on the other.
 	 */
-	xtal_clk = devm_clk_get_optional_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) {
-		u32 freq;
+	if (sitdev->out[out_idx].cmos) {
+		mask_reg = r.se_mask;
+		state_reg = r.se_state;
+	} else {
+		mask_reg = r.diff_mask;
+		state_reg = r.diff_state;
+	}
 
-		if (!device_property_read_u32(sitdev->dev, "clock-frequency",
-					      &freq))
-			sitdev->xtal_freq = freq;
+	rc = sit9531x_read_u8(sitdev, mask_reg, &mask);
+	if (rc)
+		return rc;
+	rc = sit9531x_read_u8(sitdev, state_reg, &state);
+	if (rc)
+		return rc;
+
+	*muted = (mask & BIT(r.bit)) && !(state & BIT(r.bit));
+
+	return 0;
+}
+
+/*
+ * Input priority selection
+ *
+ * The SiT9531x has an 11-slot priority table per PLL on Page 1.  Each
+ * register holds two slots nibble-packed: the earlier (even, 2n) slot
+ * in [7:4] and the later (odd, 2n+1) slot in [3:0].
+ *
+ * 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;
+}
+
+/*
+ * Rebuild a PLL's membership mask from the source codes of its priority
+ * table.  The mask is what the pin state getters test, so it is refreshed
+ * from exactly the values the table holds -- here after a write, and once
+ * per poll from the read-back in sit9531x_chan_state_fetch().
+ */
+static void sit9531x_prio_mask_build(struct sit9531x_dev *sitdev, u8 pll_idx,
+				     const u8 *srcs)
+{
+	u16 mask = 0;
+	u8 slot;
+
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++)
+		mask |= BIT(srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK);
+
+	sitdev->chan[pll_idx].prio_mask = mask;
+}
+
+/*
+ * 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);
 	}
-	if (!sitdev->xtal_freq)
-		return dev_err_probe(sitdev->dev, -EINVAL,
-				     "no xtal rate: provide clocks=<&xo> + clock-names=\"xtal\", or a clock-frequency property\n");
+
+	return 0;
+}
+
+/* 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
+
+/* The output divider is a 34-bit field */
+#define SIT9531X_DIVO_MAX			GENMASK_ULL(33, 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.
+ */
+
+/*
+ * 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;
+}
+
+/*
+ * 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;
 
 	/*
-	 * 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.
+	 * 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 modeled 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.
 	 */
-	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 */
+	if (index >= SIT9531X_MAX_INPUTS)
+		return -EINVAL;
 
-	rc = sit9531x_read_variant_id(sitdev, &variant_id);
+	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;
 
-	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);
+	if (pair & 1)
+		status >>= 4;
 
-	rc = devm_mutex_init(sitdev->dev, &sitdev->multiop_lock);
+	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 dev_err_probe(sitdev->dev, rc,
-				     "Failed to initialize mutex\n");
+		return rc;
+	rc = sit9531x_read_u8(sitdev, state_reg, &state);
+	if (rc)
+		return rc;
 
-	dev_info(sitdev->dev, "%s detected, %u inputs, %u outputs\n",
-		 sitdev->info->name, sitdev->info->num_inputs,
-		 sitdev->info->num_outputs);
+	ref->enabled = !((force & BIT(pair)) && !(state & BIT(pair)));
 
 	return 0;
 }
 
-static int sit9531x_i2c_probe(struct i2c_client *client)
+/*
+ * 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)
 {
-	struct sit9531x_dev *sitdev;
-	struct regmap *regmap;
+	enum sit9531x_signal_mode sig_mode;
+	u8 mode;
+	int rc;
 
-	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");
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_IN_MODE(pair), &mode);
+	if (rc)
+		return rc;
 
-	sitdev = devm_kzalloc(&client->dev, sizeof(*sitdev), GFP_KERNEL);
-	if (!sitdev)
-		return -ENOMEM;
+	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->dev = &client->dev;
-	sitdev->client = client;
-	sitdev->regmap = regmap;
-	i2c_set_clientdata(client, sitdev);
+	sitdev->ref[pair * 2].sig_mode = sig_mode;
+	sitdev->ref[pair * 2 + 1].sig_mode = sig_mode;
 
-	return sit9531x_dev_probe(sitdev);
+	dev_dbg(sitdev->dev, "CLKIN%u mode reg 0x%02x -> %s\n", pair, mode,
+		sig_mode == SIT9531X_MODE_DE ? "differential" : "single-ended");
+
+	return 0;
 }
 
-static const struct of_device_id sit9531x_of_match[] = {
-	{ .compatible = "sitime,sit95316" },
-	{ .compatible = "sitime,sit95317" },
-	{ }
-};
-MODULE_DEVICE_TABLE(of, sit9531x_of_match);
+/*
+ * 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];
+	u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+	u8 pll_status_1;
+	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;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_STATUS_1,
+				  &pll_status_1);
+	if (rc)
+		return rc;
+
+	/*
+	 * Which sources this PLL may select.  The table is configuration and
+	 * changes only through the driver, but reading it back keeps the
+	 * membership the pin state getters report tied to the hardware
+	 * instead of to a value the driver maintains on the side.
+	 */
+	rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+	if (rc)
+		return rc;
+
+	sit9531x_prio_mask_build(sitdev, pll_idx, srcs);
+
+	/* 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));
+	chan->ho_valid = !!(pll_status_1 & SIT9531X_PLL_STATUS_1_HO_VALID);
+
+	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_mode_fetch(sitdev, index);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_output_forced_hiz(sitdev, index, &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
+			 * signaling 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);
+		/*
+		 * The latch was not acknowledged, so with IRQF_ONESHOT the
+		 * still-asserted line would re-enter this handler back to
+		 * back forever.  Report the interrupt as unhandled so the
+		 * spurious-IRQ detector can engage if the bus stays broken.
+		 */
+		return IRQ_NONE;
+	}
+
+	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.
+ */
+/*
+ * Report what the device loaded from its EEPROM, and warn if it does not
+ * look like a healthy load.
+ *
+ * A profile that failed to load leaves the part running something other
+ * than what the board was designed around -- dividers, output routing
+ * and priority tables all differ -- while every register still reads
+ * back a plausible value.  Naming the profile and saying whether the
+ * load was clean turns that into something visible at startup instead of
+ * something inferred from measurements later.
+ *
+ * This only reports.  Boards in this family may have their
+ * configuration pushed over I2C rather than held in an EEPROM, and there
+ * the CRC pair means nothing, so a mismatch is not grounds for refusing
+ * to drive the device.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static void sit9531x_eeprom_state_report(struct sit9531x_dev *sitdev)
+{
+	u32 rec_crc = 0, cal_crc = 0, prof_id = 0;
+	u8 notif, v;
+	int rc, i;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	/* Profile id: three bytes, least significant first. */
+	for (i = 2; i >= 0; i--) {
+		rc = sit9531x_read_u8(sitdev, SIT9531X_REG_PROFILE_ID + i, &v);
+		if (rc)
+			return;
+		prof_id = prof_id << 8 | v;
+	}
+
+	dev_info(sitdev->dev, "profile id %u\n", prof_id);
+
+	/* Both CRCs: four bytes, most significant first. */
+	for (i = 0; i < 4; i++) {
+		rc = sit9531x_read_u8(sitdev, SIT9531X_REG_REC_CRC + i, &v);
+		if (rc)
+			return;
+		rec_crc = rec_crc << 8 | v;
+
+		rc = sit9531x_read_u8(sitdev, SIT9531X_REG_CAL_CRC + i, &v);
+		if (rc)
+			return;
+		cal_crc = cal_crc << 8 | v;
+	}
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_EEPROM_NOTIF, &notif);
+	if (rc)
+		return;
+
+	/*
+	 * A clean load leaves the read-done bit set and every defect bit
+	 * clear.  Both CRCs zero means no EEPROM read happened at all:
+	 * boards in this family may take their configuration over I2C
+	 * instead of an EEPROM, and there the CRC pair means nothing, so
+	 * that case is not a mismatch worth warning about.
+	 */
+	if (!rec_crc && !cal_crc)
+		dev_dbg(sitdev->dev,
+			"no EEPROM profile (configuration pushed over I2C)\n");
+	else if (rec_crc != cal_crc)
+		dev_warn(sitdev->dev,
+			 "EEPROM CRC mismatch: stored %08x, computed %08x\n",
+			 rec_crc, cal_crc);
+	else if (notif != SIT9531X_EEPROM_READ_DONE)
+		dev_warn(sitdev->dev,
+			 "EEPROM read reported defects (notify %02x)\n",
+			 notif);
+	else
+		dev_dbg(sitdev->dev, "EEPROM profile loaded, CRC %08x\n",
+			cal_crc);
+}
+
+/*
+ * Report which PLLs came up, and flag the one case that is a real
+ * inconsistency rather than a configuration choice.
+ *
+ * A PLL the loaded configuration leaves unused never reaches its active
+ * state, which is normal and not worth a warning.  A PLL that has
+ * outputs routed to it and is still not active is different: something
+ * that is meant to be generating clocks is not running, and every value
+ * read from it -- lock state, phase, frequency offset -- describes a
+ * stopped loop.  Say so once at startup rather than leaving it to be
+ * discovered through measurements that quietly read as zero.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static void sit9531x_pll_states_report(struct sit9531x_dev *sitdev)
+{
+	unsigned int idx, i;
+	bool routed;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		if (sitdev->chan[i].active) {
+			dev_dbg(sitdev->dev, "PLL%c active\n", 'A' + i);
+			continue;
+		}
+
+		routed = false;
+		for (idx = 0; idx < sitdev->info->num_outputs; idx++) {
+			const struct sit9531x_out *out;
+
+			out = sit9531x_out_state_get(sitdev, idx);
+			if (out->routed && out->pll_idx == i) {
+				routed = true;
+				break;
+			}
+		}
+
+		if (routed)
+			dev_warn(sitdev->dev,
+				 "PLL%c drives outputs but is not in its active state\n",
+				 'A' + i);
+		else
+			dev_dbg(sitdev->dev, "PLL%c unused by the loaded configuration\n",
+				'A' + i);
+	}
+}
+
+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;
+
+	mutex_lock(&sitdev->multiop_lock);
+	sit9531x_eeprom_state_report(sitdev);
+	sit9531x_pll_states_report(sitdev);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	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_dpll_pin_is_registrable - check if a pin should be registered
+ * @dir:	pin direction
+ * @index:	pin hardware index
+ *
+ * Only the XO pin has a complete pin-op table in this patch, so only
+ * the XO pin is registrable here.  Other pin classes are registered
+ * once their state callbacks land in the following patches.
+ *
+ * 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)
+{
+	/*
+	 * Only the XO pin has a complete pin-op table in this patch.
+	 * Other pin classes are registered once their state callbacks
+	 * land in the following patches.
+	 */
+	if (dir != DPLL_PIN_DIRECTION_INPUT)
+		return false;
+
+	return index == SIT9531X_MAX_INPUTS;
+}
+
+/*
+ * 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, the I2C bus number and the I2C address.  The bus number
+ * disambiguates two same-variant parts at the same address on
+ * different adapters (or behind a mux), which the DPLL core would
+ * otherwise fold onto one set of objects; DT bus aliases keep the
+ * numbering, and with it the clock_id, stable 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)(i2c_adapter_id(sitdev->client->adapter) & 0xff) << 16;
+	clkid |= (u64)sitdev->info->id << 8;
+	clkid |= (u64)sitdev->client->addr;
+
+	return clkid;
+}
+
+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.  The rate normally comes from a
+	 * "clocks" phandle (clock-names = "xtal").  On platforms where the
+	 * firmware does not expose the XO through the clock framework, fall
+	 * back to a "clock-frequency" device property.
+	 */
+	xtal_clk = devm_clk_get_optional_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) {
+		u32 freq;
+
+		if (!device_property_read_u32(sitdev->dev, "clock-frequency",
+					      &freq))
+			sitdev->xtal_freq = freq;
+	}
+	if (!sitdev->xtal_freq)
+		return dev_err_probe(sitdev->dev, -EINVAL,
+				     "no xtal rate: provide clocks=<&xo> + clock-names=\"xtal\", or a clock-frequency property\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 */
+
+	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);
+
+/*
+ * The poll worker is not freezable and would keep issuing paged I2C
+ * transfers into a suspended adapter, where i2c_transfer() fails and a
+ * tick landing mid-suspend could tear a paged sequence between the
+ * page-selector write and the register access.  Park the worker (and
+ * the IRQ that kicks it) across suspend and take a fresh sample on
+ * resume.
+ */
+static int sit9531x_suspend(struct device *dev)
+{
+	struct sit9531x_dev *sitdev = dev_get_drvdata(dev);
+
+	if (sitdev->irq > 0)
+		disable_irq(sitdev->irq);
+	kthread_cancel_delayed_work_sync(&sitdev->work);
+
+	return 0;
+}
+
+static int sit9531x_resume(struct device *dev)
+{
+	struct sit9531x_dev *sitdev = dev_get_drvdata(dev);
+
+	if (sitdev->irq > 0)
+		enable_irq(sitdev->irq);
+	kthread_queue_delayed_work(sitdev->kworker, &sitdev->work, 0);
+
+	return 0;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(sit9531x_pm_ops,
+				sit9531x_suspend, sit9531x_resume);
 
 static struct i2c_driver sit9531x_i2c_driver = {
 	.driver			= {
 		.name		= "sit9531x",
 		.of_match_table	= sit9531x_of_match,
+		.pm		= pm_sleep_ptr(&sit9531x_pm_ops),
 	},
 	.probe		= sit9531x_i2c_probe,
 };
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 230b21b9e238..5ead3811e14b 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -15,6 +15,8 @@
 
 #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>
@@ -25,6 +27,8 @@
 #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 modeled as two pins.  The
  * destination PLL that locks to INTSYNC sees an input pin
@@ -34,6 +38,15 @@
  */
 #define SIT9531X_INTSYNC_PIN_ID		(SIT9531X_MAX_INPUTS + 1)
 #define SIT9531X_INTSYNC_OUT_PIN_ID	SIT9531X_MAX_OUTPUTS
+#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
@@ -64,37 +77,97 @@ enum sit9531x_signal_mode {
 /*
  * 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;
-	const char			*label;
+	u64		freq;
+	bool		enabled;
+	bool		los;
+	bool		oof;
+	u8		pll_mask;
+	const char	*label;
 	enum sit9531x_signal_mode	sig_mode;
 };
 
 /*
  * struct sit9531x_out - output state
- * @freq:		configured frequency in Hz
+ * @enabled:		output is driving, i.e. not forced into Hi-Z
+ * @cmos:		output is wired single-ended; the Hi-Z pair that
+ *			speaks for it is the SE one, not the differential
+ * @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;
+	u64		freq;
+	bool		enabled;
+	bool		cmos;
+	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
+ * @ho_valid:		holdover memory acquired, i.e. the holdover window
+ *			holds a valid estimate to fall back on
+ * @prio_mask:		bit per hardware source code present in this PLL's
+ *			priority table, i.e. the sources it may select.  Read
+ *			back from the table by the periodic worker and
+ *			refreshed by every table write, so it tracks the
+ *			hardware rather than the driver's intent
+ */
+struct sit9531x_chan {
+	bool		active;
+	bool		locked;
+	u8		mode;
+	u8		selected_ref;
+	bool		inner_lol;
+	bool		ho_freeze;
+	bool		ho_valid;
+	u16		prio_mask;
+};
+
 /*
  * struct sit9531x_dev - SiT9531x device instance
- * @dev:		parent device
- * @client:		I2C client
- * @regmap:		paged register map
  * @info:		detected chip variant info
- * @multiop_lock:	serializes multi-register sequences
+ * @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;
@@ -107,11 +180,107 @@ struct sit9531x_dev {
 	/* 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, ...).
@@ -137,27 +306,78 @@ static inline bool sit9531x_input_is_n(u8 index)
 }
 
 /*
- * sit9531x_pll_page - get register page for PLL index
- * @pll_idx: PLL index (0 = PLLA, 3 = PLLD)
+ * 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_pll_page(u8 pll_idx)
+static inline u8 sit9531x_input_hw_src(u8 index)
 {
-	return SIT9531X_PAGE_PLLA + pll_idx;
+	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);
 }
 
-extern const struct regmap_config sit9531x_regmap_config;
+/*
+ * 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;
+}
 
-/* ---- Core lifecycle ---- */
-int  sit9531x_dev_probe(struct sit9531x_dev *sitdev);
+/*
+ * 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];
+}
 
-/* ---- 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);
+/*
+ * 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..b4984335e6bf
--- /dev/null
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -0,0 +1,444 @@
+// 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 bool sit9531x_dpll_is_input_pin(const struct sit9531x_dpll_pin *pin)
+{
+	return pin->dir == DPLL_PIN_DIRECTION_INPUT;
+}
+
+static 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), the holdover freeze byte (reg 0x0A) and the per-PLL
+ * holdover-valid bit (PLL page, reg 0x06).
+ */
+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->mode) {
+		/*
+		 * Free-run: the outer loop is disabled, so the PLL tracks no
+		 * reference at all and its loss-of-lock bit means nothing.
+		 * That is what UNLOCKED describes -- "not yet locked to any
+		 * valid input (or was forced by user)".
+		 */
+		*status = DPLL_LOCK_STATUS_UNLOCKED;
+	} else if (chan->locked) {
+		/*
+		 * HO_ACQ is locked *and* holdover memory acquired, so it needs
+		 * the holdover-valid bit rather than following from the lock.
+		 */
+		if (chan->ho_valid)
+			*status = DPLL_LOCK_STATUS_LOCKED_HO_ACQ;
+		else
+			*status = DPLL_LOCK_STATUS_LOCKED;
+	} 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;
+}
+
+/*
+ * Mode
+ * ====
+ * enum dpll_mode differentiates how a DPLL selects an input: AUTOMATIC
+ * has the device pick the highest-priority one, MANUAL has userspace
+ * request one.  The device only implements the former through this
+ * driver, so AUTOMATIC is the only mode advertised.
+ *
+ * Free-run -- the outer loop disabled through PLL page reg 0x31[5] -- is
+ * not a mode in those terms, because no input is selected either way.  It
+ * is reported through lock status instead, and reached through the
+ * chip-specific tool rather than over netlink.
+ *
+ * The device could implement real MANUAL: MISCINNER_PLL (PLL page reg
+ * 0x18) bit 5 switches a PLL from priority-based to manual active select,
+ * and GPIO_INPUT_FUNC_CTRL5..8 (page 0, regs 0xE8-0xEB) bit 4 makes the
+ * choice come from the register's own low nibble instead of the GPIO
+ * pins, which pins one reference while the loop keeps running.  Wiring
+ * that up would let .state_on_dpll_set() accept CONNECTED; it needs bench
+ * validation first, and reg 0x18 carries GUI-generated configuration in
+ * its other bits, so it is left out until then.  Advertising MANUAL and
+ * then refusing the one request MANUAL exists for is the worse of the two
+ * incomplete answers, and after merge it would be ABI.
+ */
+static int
+sit9531x_dpll_mode_get(const struct dpll_device *dpll, void *dpll_priv,
+		       enum dpll_mode *mode, struct netlink_ext_ack *extack)
+{
+	*mode = DPLL_MODE_AUTOMATIC;
+
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_mode_set - put the PLL in automatic selection mode
+ *
+ * Clears the outer loop disable bit (PLL page reg 0x31[5]) and triggers a
+ * small update via reg 0x0F, so a PLL left free-running by the loaded
+ * configuration or by the chip-specific tool returns to selecting its
+ * reference from the priority table.  Any other mode is refused.
+ */
+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;
+	int rc, restore_rc;
+	u8 status;
+
+	if (mode != DPLL_MODE_AUTOMATIC) {
+		NL_SET_ERR_MSG(extack,
+			       "Device selects its reference by priority; only automatic mode is supported");
+		return -EOPNOTSUPP;
+	}
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	/*
+	 * Read before writing.  Automatic is the only mode this driver
+	 * advertises, so userspace setting it again is a no-op the device
+	 * must not feel: without this, a redundant set followed by one
+	 * failed latch would restore an outer-loop disable the PLL never
+	 * had and put a running loop into free-run.
+	 */
+	rc = sit9531x_read_pll_u8(sitdev, sitdpll->id,
+				  SIT9531X_PLL_REG_STATUS, &status);
+	if (rc) {
+		NL_SET_ERR_MSG(extack, "Failed to read PLL mode register");
+		goto unlock;
+	}
+	if (!(status & SIT9531X_PLL_STATUS_OUTER_DIS)) {
+		sitdev->chan[sitdpll->id].mode = 0;
+		goto unlock;
+	}
+
+	rc = sit9531x_update_pll_u8(sitdev, sitdpll->id,
+				    SIT9531X_PLL_REG_STATUS,
+				    SIT9531X_PLL_STATUS_OUTER_DIS, 0);
+	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) {
+		/*
+		 * The cleared disable bit was never latched.  Put it back so
+		 * the register does not read "outer loop enabled" for a loop
+		 * that is still free-running, which the poll would then hand
+		 * to the lock-status getter as a live mode.
+		 */
+		restore_rc = sit9531x_update_pll_u8(sitdev, sitdpll->id,
+						    SIT9531X_PLL_REG_STATUS,
+						    SIT9531X_PLL_STATUS_OUTER_DIS,
+						    SIT9531X_PLL_STATUS_OUTER_DIS);
+		if (restore_rc)
+			dev_warn(sitdev->dev,
+				 "PLL%c outer loop left enabled without a latch: %d\n",
+				 'A' + sitdpll->id, restore_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 the pin state getters and lock status
+	 * read this cache and would otherwise keep reporting free-run until
+	 * the next poll.
+	 */
+	sitdev->chan[sitdpll->id].mode = 0;
+
+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);
+
+	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 */
+};
+
+/*
+ * Pin-state contract
+ * ==================
+ * The five pin ops tables below fall into three roles, and only the first
+ * has a selection state machine.  Each state_on_dpll callback implements
+ * the rules for its role and nothing else, so the tables cannot drift
+ * apart the way five independent encodings of this did.
+ *
+ * SELECTION role -- physical input pins, INTSYNC destination pin.
+ *   Where does this reference sit in this DPLL's selection process?
+ *   Predicates, all evaluated under multiop_lock:
+ *     M  source is present in THIS PLL's hardware priority table
+ *     A  chan->mode == 0                      (outer loop running)
+ *     L  chan->locked && !chan->inner_lol     (only meaningful when A)
+ *     S  chan->selected_ref == this pin's id
+ *   get:
+ *     CONNECTED     A && L && S && M
+ *     SELECTABLE    M && !(A && L && S)
+ *     DISCONNECTED  !M
+ *   set:
+ *     DISCONNECTED  remove from this PLL's table; a physical input also
+ *                   releases this DPLL's claim and powers the shared
+ *                   receiver down on the last release
+ *     SELECTABLE    add to this PLL's table; a physical input powers the
+ *                   receiver up and takes the claim, in that order
+ *     CONNECTED     -EOPNOTSUPP -- the device selects by priority and has
+ *                   no mode that pins one reference (see mode_set())
+ *     other         -EINVAL
+ *
+ *   A is what separates CONNECTED from SELECTABLE: chan->locked is derived
+ *   from the outer loss-of-lock bit, which can read clear while the outer
+ *   loop is disabled, so a free-running PLL would otherwise claim its
+ *   reference as the active input of a loop that is ignoring it.
+ *
+ *   M is read from the hardware priority table, not from ref->pll_mask.
+ *   pll_mask is only the shared-receiver refcount and says nothing about
+ *   one DPLL's eligibility; it also drifts, because .prio_set writes the
+ *   table without touching it.  Signal quality is reported through the
+ *   pin's own attributes rather than by demoting the state, so a source
+ *   that is momentarily in LOS stays selectable.
+ *
+ * DRIVE role -- output pins, INTSYNC source pin.
+ *   Is this pin or net being driven?  Nothing is selected here, so:
+ *     CONNECTED     pin or net is driven
+ *     DISCONNECTED  pin is muted (Hi-Z), or this PLL does not drive it
+ *     SELECTABLE    -EINVAL on set, never reported by get
+ *
+ * FIXED role -- XO pin.  Always CONNECTED; it cannot be routed.
+ */
+
+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;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
+	.direction_get		= sit9531x_dpll_input_pin_direction_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);
+
+/* ---- INTSYNC destination (input) pin ---- */
+
+/*
+ * 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,
+	.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;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_output_pin_ops = {
+	.direction_get		= sit9531x_dpll_output_pin_direction_get,
+};
+
+const struct dpll_pin_ops *
+sit9531x_dpll_pin_ops_get(const struct sit9531x_dpll_pin *pin)
+{
+	if (!sit9531x_dpll_is_input_pin(pin))
+		return &sit9531x_dpll_output_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..901aec238f4f
--- /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;
+	/* in 1/DPLL_PHASE_OFFSET_DIVIDER picosecond units */
+	s64				phase_offset;
+	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/regs.h b/drivers/dpll/sit9531x/regs.h
index ae553bec0fd8..a892948da38d 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -49,7 +49,237 @@
  */
 #define SIT9531X_REG_VARIANT_ID		SIT9531X_REG(0x00, 0x02)
 
-/* Variant ID values (page 0 reg 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,
+ *   5=OCXO, 6=INTSYNC,
+ *   7=IN0N, 8=IN1N, 9=IN2N, 10=IN3N
+ *
+ * Codes 4 and 11 address a fifth input pair that this part does
+ * not have.  They read back as no valid reference.
+ */
+#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 into the volatile registers
+ *   bit 2  program the efuse
+ *   bit 1  small change update (SIT9531X_SMALL_UPDATE_CMD)
+ *   bit 0  escape to the PRG_CMD state
+ *
+ * The NVM bank is a volatile shadow, so bits 4 and 1 are both fine in
+ * a runtime path: bit 1 for a change made in the active state, bit 4
+ * to close a PRG_CMD sequence.  Only bit 2 writes non-volatile
+ * storage, and the driver never issues it.
+ */
+#define SIT9531X_REG_GLOBAL_UPDATE		SIT9531X_REG(0x00, 0x0F)
+#define SIT9531X_SMALL_UPDATE_CMD		0x02
+
+/* 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 driver configuration.  Either CMOS enable means the output
+ * is wired single-ended -- one lane, or both driven as CMOS; with
+ * neither set it is a differential pair.
+ */
+#define SIT9531X_OUT_MISC0_BASE		0x1E
+#define SIT9531X_OUT_MISC0_STRIDE		0x10
+#define SIT9531X_OUT_CMOS_ENP		BIT(3)
+#define SIT9531X_OUT_CMOS_ENN		BIT(2)
+
+/*
+ * 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
+
+/*
+ * 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
+
+/*
+ * 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
+
+#define SIT9531X_PLL_REG_ACTIVE		0x02
+#define SIT9531X_PLL_ACTIVE_BIT		BIT(0)  /* PLL reached active state */
+#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 STATUS register bits */
+#define SIT9531X_PLL_STATUS_LOCK		BIT(0)
+#define SIT9531X_PLL_STATUS_OUTER_DIS	BIT(5)
+
+/*
+ * Per-PLL status register.  HO_VALID says the holdover window holds a
+ * valid frequency estimate, i.e. holdover memory has been acquired; it is
+ * not the same as HO_FREEZE (page 0, reg 0x0A), which says the PLL has
+ * already switched over to holdover.
+ */
+#define SIT9531X_PLL_REG_STATUS_1		0x06
+#define SIT9531X_PLL_STATUS_1_HO_VALID	BIT(2)
+
+/* 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
+
+/*
+ * EEPROM profile load, page 0.  The device compares the CRC stored in the
+ * EEPROM against the one it computes from what it read; a mismatch means
+ * the profile on the part is not the profile the board expects.
+ * NOTIFY_4 collects the read-done bit and the defect bits, all sticky, so
+ * a healthy load leaves exactly the read-done bit set.
+ */
+#define SIT9531X_REG_REC_CRC		SIT9531X_REG(0x00, 0x8A)  /* 4 bytes, MSB first */
+#define SIT9531X_REG_CAL_CRC		SIT9531X_REG(0x00, 0x8E)  /* 4 bytes, MSB first */
+#define SIT9531X_REG_EEPROM_NOTIF		SIT9531X_REG(0x00, 0x97)
+#define SIT9531X_EEPROM_READ_DONE		BIT(0)
+
+/* Profile identifier the loaded configuration carries, page 1, 24 bits */
+#define SIT9531X_REG_PROFILE_ID		SIT9531X_REG(0x01, 0x44)  /* 3 bytes, LSB first */
+
+/* ---- Variant ID values (single byte read from SIT9531X_REG_VARIANT_ID) ---- */
 #define SIT9531X_VARIANT_ID_95317	0x17
 #define SIT9531X_VARIANT_ID_95316	0x31
 
-- 
2.39.2 (Apple Git-143)


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

* [PATCH net-next v9 06/15] dpll: sit9531x: implement input pin state on a DPLL
  2026-09-15  0:00 [PATCH net-next v9 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (4 preceding siblings ...)
  2026-09-15  0:00 ` [PATCH net-next v9 04/15] dpll: sit9531x: read DPLL types and pin properties from system firmware Ali Rouhi
@ 2026-09-15  0:00 ` Ali Rouhi
  2026-09-17 15:01   ` netdev-bot+sashiko
  2026-09-15  0:00 ` [PATCH net-next v9 07/15] dpll: sit9531x: add support to get and set priority on input pins Ali Rouhi
                   ` (8 subsequent siblings)
  14 siblings, 1 reply; 33+ messages in thread
From: Ali Rouhi @ 2026-09-15  0:00 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel, Ali Rouhi

From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>

Report and set whether an input feeds a given PLL.  The device selects
its reference itself from a priority table, so membership of that table is
what "connected" means here: an input in the table is selectable, the one
the PLL has locked to is connected, and an input absent from the table is
disconnected.  Setting the state therefore adds the input to the table or
removes it, and the table is read back rather than remembered.

The pins fall into three roles and only the first answers this question,
so the contract for all of them is written above the operations rather
than left to be inferred: inputs and the inter-PLL sync destination take
part in selection; an output and the sync source are driven by the PLL and
report whether they carry a signal; the crystal is fixed and always
connected, since a PLL cannot be told to stop using it.

Rewriting the table means forcing the PLL into holdover, waiting for it to
take, writing every slot and releasing holdover again, all under the
device lock -- ten to twenty milliseconds.  The device has no way to
change one slot in isolation, and holdover is what keeps the loop from
chasing a table that is momentarily inconsistent, so the sequence is the
cost of doing it correctly.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/core.c | 501 ++++++++++++++++++++++++++++++++++-
 drivers/dpll/sit9531x/core.h |   2 +
 drivers/dpll/sit9531x/dpll.c | 171 ++++++++++++
 drivers/dpll/sit9531x/regs.h |   4 +
 4 files changed, 669 insertions(+), 9 deletions(-)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 6dfb1b31d491..25418485e975 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -185,6 +185,93 @@ static void sit9531x_input_get_regs(const struct sit9531x_dev *sitdev,
 	}
 }
 
+/*
+ * 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.
+ *
+ * Both writes are attempted even when the first fails, and the first
+ * error is returned: the force and state bits only mean something
+ * together, so a half-programmed override is not left behind on a
+ * transient bus error.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_input_disable(struct sit9531x_dev *sitdev, u8 index)
+{
+	unsigned int force_reg, state_reg;
+	struct sit9531x_ref *ref;
+	u8 pair, val;
+	int rc, ret;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (index >= SIT9531X_MAX_INPUTS)
+		return -EINVAL;
+
+	ref = &sitdev->ref[index];
+	pair = sit9531x_input_pair(index);
+	sit9531x_input_get_regs(sitdev, index, &force_reg, &state_reg);
+
+	rc = sit9531x_read_u8(sitdev, force_reg, &val);
+	if (!rc)
+		rc = sit9531x_write_u8(sitdev, force_reg, val | BIT(pair));
+
+	ret = sit9531x_read_u8(sitdev, state_reg, &val);
+	if (!ret)
+		ret = sit9531x_write_u8(sitdev, state_reg, val & ~BIT(pair));
+	if (ret && !rc)
+		rc = ret;
+
+	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).
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_input_enable(struct sit9531x_dev *sitdev, u8 index)
+{
+	unsigned int force_reg, state_reg;
+	struct sit9531x_ref *ref;
+	u8 pair, val;
+	int rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (index >= SIT9531X_MAX_INPUTS)
+		return -EINVAL;
+
+	ref = &sitdev->ref[index];
+	pair = sit9531x_input_pair(index);
+	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)
  *
@@ -347,6 +434,92 @@ static u8 sit9531x_prio_slot_get(u8 val, u8 slot)
 	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 documented 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;
+}
+
+/*
+ * Fold a source code to the lane a DPLL pin actually represents.
+ *
+ * Differential input pairs expose only the P lane as a DPLL pin.  A
+ * priority table entry encoded as an N lane for such a pair must map to
+ * the P-lane source for pin-facing operations (membership, priority slots,
+ * add/remove/set lookups), matching sit9531x_ref_pll_mask_fetch().
+ */
+static u8 sit9531x_prio_src_canon(const struct sit9531x_dev *sitdev, u8 src)
+{
+	u8 index = sit9531x_hw_src_input(src);
+
+	if (index >= sitdev->info->num_inputs)
+		return src;
+
+	if (sit9531x_input_is_n(index) &&
+	    sitdev->ref[index].sig_mode == SIT9531X_MODE_DE)
+		return sit9531x_input_hw_src(index - 1);
+
+	return src;
+}
+
+/*
+ * sit9531x_input_prio_present - is a source listed in a PLL's priority table
+ * @input_idx:	input source in hardware encoding (see
+ *		sit9531x_input_hw_src())
+ *
+ * Answers from the membership mask that every table write and every poll
+ * refreshes, which is what the pin state getters test.  The priority slot
+ * cannot answer this: a source that is not in the table reports the lowest
+ * slot, so the slot value alone does not separate absent from last.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+bool sit9531x_input_prio_present(struct sit9531x_dev *sitdev, u8 pll_idx,
+				 u8 input_idx)
+{
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return false;
+
+	input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+	if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+		return false;
+
+	return !!(sitdev->chan[pll_idx].prio_mask & BIT(input_idx));
+}
+
 /*
  * Rebuild a PLL's membership mask from the source codes of its priority
  * table.  The mask is what the pin state getters test, so it is refreshed
@@ -365,6 +538,90 @@ static void sit9531x_prio_mask_build(struct sit9531x_dev *sitdev, u8 pll_idx,
 	sitdev->chan[pll_idx].prio_mask = mask;
 }
 
+/* Attempts to release a forced holdover before reporting it stuck. */
+#define SIT9531X_HO_CLEAR_TRIES		3
+
+/*
+ * 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, attempt;
+	int rc, prg_rc, ho_rc = 0;
+	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: slots written before a failed write are in
+	 * the table regardless, so the latch keeps hardware and the mask
+	 * rebuild below consistent with what was actually written.
+	 */
+	prg_rc = sit9531x_prio_prg_commit(sitdev);
+	if (prg_rc && !rc)
+		rc = prg_rc;
+
+	/*
+	 * Refresh the mask from the table just written so a get that follows
+	 * a set does not have to wait for the next poll.  Slots written
+	 * before a failed write are in the table too, so this is closer to
+	 * the truth than the pre-write mask either way, and the poll read-back
+	 * corrects whatever a partial write left behind.
+	 */
+	sit9531x_prio_mask_build(sitdev, pll_idx, srcs);
+
+	/*
+	 * Release the forced holdover.  Nothing else in the driver clears
+	 * this bit, so a PLL left with it set reports holdover until the
+	 * next table write on the same PLL happens to clear it, which may
+	 * never come.  Retry before giving up, and say so if it stays set.
+	 */
+	for (attempt = 0; attempt < SIT9531X_HO_CLEAR_TRIES; attempt++) {
+		ho_rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+					       SIT9531X_PLL_REG_HO_CTRL,
+					       BIT(SIT9531X_PLL_HO_FORCE_BIT),
+					       0);
+		if (!ho_rc)
+			break;
+		usleep_range(1000, 2000);
+	}
+	if (ho_rc) {
+		dev_err(sitdev->dev, "PLL%c left in forced holdover: %d\n",
+			'A' + pll_idx, ho_rc);
+		if (!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
@@ -389,6 +646,208 @@ static int sit9531x_prio_table_read(struct sit9531x_dev *sitdev, u8 pll_idx,
 	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.
+ *
+ * The table is what makes a source eligible for this PLL, so this only
+ * ever reorders sources already in it.  A source that is absent is
+ * disconnected on this PLL, and inserting it here would make it a
+ * selection candidate again behind the caller's back; that is a connect,
+ * and it belongs to the pin's state setter.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, -EINVAL if the source is not in the table,
+ * <0 on error
+ */
+int sit9531x_input_prio_set(struct sit9531x_dev *sitdev, u8 pll_idx,
+			    u8 input_idx, u8 prio)
+{
+	u8 orig_srcs[SIT9531X_PRIO_MAX_SLOTS];
+	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;
+	input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+	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;
+
+	memcpy(orig_srcs, srcs, sizeof(srcs));
+
+	for (from = 0; from < SIT9531X_PRIO_MAX_SLOTS; from++)
+		if (sit9531x_prio_src_canon(sitdev, srcs[from]) == input_idx)
+			break;
+
+	if (from == SIT9531X_PRIO_MAX_SLOTS)
+		return -EINVAL;
+
+	if (from == prio)
+		return 0;
+
+	if (from > prio) {
+		/* Moving up: push the entries in between down one slot. */
+		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;
+	if (!memcmp(srcs, orig_srcs, sizeof(srcs)))
+		return 0;
+
+	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,
+ * since the 4-bit slot encoding has no "empty" value.  This makes a
+ * disconnected input ineligible for automatic reference selection, not
+ * just gated at the input buffer.
+ *
+ * Removing a source that is absent is what the caller asked for already,
+ * so it succeeds without touching the table.  Removing the only source
+ * would leave the table empty, which the device does not accept; that
+ * fails with -EBUSY rather than reporting a success the hardware never
+ * carried out.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, -EBUSY if the source is the only entry, <0 on
+ * error
+ */
+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;
+	input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+
+	rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+	if (rc)
+		return rc;
+
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+		if (sit9531x_prio_src_canon(sitdev, srcs[slot]) == input_idx)
+			found = true;
+		else
+			kept[count++] = srcs[slot];
+	}
+
+	if (!found)
+		return 0;
+
+	if (count == 0)
+		return -EBUSY;
+
+	/* 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
+ * replaces a duplicate at the tail.  A table that contains only unique
+ * sources cannot accept a new one without evicting another, so that
+ * case fails with -ENOSPC.
+ *
+ * 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 seen[SIT9531X_PRIO_NUM_SRC] = { 0 };
+	u8 replace = SIT9531X_PRIO_MAX_SLOTS;
+	u8 slot, src, src_canon;
+	int rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+	input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+	if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+		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 (sit9531x_prio_src_canon(sitdev, srcs[slot]) == input_idx)
+			return 0;
+
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+		src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
+		src_canon = sit9531x_prio_src_canon(sitdev, src);
+		if (src_canon >= SIT9531X_PRIO_NUM_SRC)
+			continue;
+
+		seen[src_canon]++;
+	}
+
+	for (slot = SIT9531X_PRIO_MAX_SLOTS; slot-- > 0;) {
+		src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
+		src_canon = sit9531x_prio_src_canon(sitdev, src);
+		if (src_canon >= SIT9531X_PRIO_NUM_SRC)
+			continue;
+
+		if (seen[src_canon] > 1) {
+			replace = slot;
+			break;
+		}
+	}
+
+	if (replace == SIT9531X_PRIO_MAX_SLOTS)
+		return -ENOSPC;
+
+	srcs[replace] = input_idx;
+
+	return sit9531x_prio_table_commit(sitdev, pll_idx, srcs);
+}
+
 /* XO doubler register */
 #define SIT9531X_REG_XO2_GENERIC		SIT9531X_REG(0x00, 0x2D)
 #define SIT9531X_XO_DOUBLER_ENB_BIT		7   /* inverted: 0 = enabled */
@@ -1308,14 +1767,38 @@ static void sit9531x_dpll_pins_unregister(struct sit9531x_dpll *sitdpll)
 	}
 }
 
+/*
+ * 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
  *
- * Only the XO pin has a complete pin-op table in this patch, so only
- * the XO pin is registrable here.  Other pin classes are registered
- * once their state callbacks land in the following patches.
+ * 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
  */
@@ -1323,15 +1806,15 @@ static bool sit9531x_dpll_pin_is_registrable(struct sit9531x_dpll *sitdpll,
 					     enum dpll_pin_direction dir,
 					     u8 index)
 {
-	/*
-	 * Only the XO pin has a complete pin-op table in this patch.
-	 * Other pin classes are registered once their state callbacks
-	 * land in the following patches.
-	 */
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+
 	if (dir != DPLL_PIN_DIRECTION_INPUT)
 		return false;
 
-	return index == SIT9531X_MAX_INPUTS;
+	if (index == SIT9531X_MAX_INPUTS)
+		return true;
+
+	return sit9531x_input_pin_is_registrable(sitdev, index);
 }
 
 /*
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 5ead3811e14b..f628bd5b7a97 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -231,6 +231,8 @@ int sit9531x_input_disable(struct sit9531x_dev *sitdev, u8 index);
 int sit9531x_input_enable(struct sit9531x_dev *sitdev, u8 index);
 
 /* ---- Input priority ---- */
+bool sit9531x_input_prio_present(struct sit9531x_dev *sitdev,
+				 u8 pll_idx, u8 input_idx);
 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,
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index b4984335e6bf..ab9aa7db1839 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -288,6 +288,41 @@ const struct dpll_device_ops sit9531x_dpll_device_ops = {
  * FIXED role -- XO pin.  Always CONNECTED; it cannot be routed.
  */
 
+/*
+ * Report a selection-role pin's state on this DPLL.  @pin_id is a logical
+ * input index, SIT9531X_INTSYNC_PIN_ID for the INTSYNC destination.
+ *
+ * Membership comes from chan->prio_mask, which is the priority table read
+ * back from the chip -- not a record of what the driver asked for.  The
+ * getter runs on every poll for every input pin of every DPLL, so it takes
+ * the mask the worker refreshed rather than rescanning the table over I2C
+ * each time; table writes refresh it too, so a get right after a set does
+ * not report the old membership.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static void
+sit9531x_dpll_selection_state_get(struct sit9531x_dev *sitdev,
+				  const struct sit9531x_dpll *sitdpll,
+				  u8 pin_id, enum dpll_pin_state *state)
+{
+	const struct sit9531x_chan *chan;
+	bool active_input;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
+	active_input = !chan->mode && chan->locked && !chan->inner_lol &&
+		       chan->selected_ref == pin_id;
+
+	if (!(chan->prio_mask & BIT(sit9531x_input_hw_src(pin_id))))
+		*state = DPLL_PIN_STATE_DISCONNECTED;
+	else if (active_input)
+		*state = DPLL_PIN_STATE_CONNECTED;
+	else
+		*state = DPLL_PIN_STATE_SELECTABLE;
+}
+
 static int
 sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
 				      void *pin_priv,
@@ -300,8 +335,144 @@ sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
 	return 0;
 }
 
+/*
+ * sit9531x_dpll_input_pin_state_on_dpll_get - get input pin DPLL state
+ *
+ * Selection role; see the pin-state contract above.
+ */
+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;
+
+	mutex_lock(&sitdev->multiop_lock);
+	sit9531x_dpll_selection_state_get(sitdev, sitdpll, dpin->id, state);
+	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.
+ * Selection role; see the pin-state contract above for the states.
+ *
+ * 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);
+		/*
+		 * The table write, the latch and the holdover release are
+		 * three steps behind one return code, so ask the table what
+		 * actually happened rather than reading the errno as "no
+		 * change".  A source that is gone from the table has been
+		 * released whatever else failed.
+		 */
+		if (rc && sit9531x_input_prio_present(sitdev, sitdpll->id,
+						      hw_src))
+			break;
+		ref->pll_mask &= ~pll_bit;
+		if (rc)
+			break;
+		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, which the
+		 * device cannot be told to do: it selects by priority and the
+		 * manual-active-select path is not wired up (see mode_set()).
+		 * 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:
+		if (!(ref->pll_mask & pll_bit)) {
+			rc = sit9531x_input_enable(sitdev, dpin->id);
+			if (rc)
+				break;
+		}
+		rc = sit9531x_input_prio_add(sitdev, sitdpll->id, hw_src);
+		if (rc && !sit9531x_input_prio_present(sitdev, sitdpll->id,
+						       hw_src)) {
+			if (!(ref->pll_mask & pll_bit) && !ref->pll_mask)
+				sit9531x_input_disable(sitdev, dpin->id);
+			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);
+
+	/*
+	 * Leave the messages the switch already set in place; only a failure
+	 * that came from the hardware path still needs one.
+	 */
+	if (rc == -EBUSY)
+		NL_SET_ERR_MSG(extack,
+			       "Only source left in the priority table; it cannot be emptied");
+	else if (rc == -ENOSPC)
+		NL_SET_ERR_MSG(extack,
+			       "Priority table is full of unique sources on this PLL");
+	else if (rc && rc != -EOPNOTSUPP && rc != -EINVAL)
+		NL_SET_ERR_MSG(extack, "Failed to set input pin state");
+
+	return rc;
+}
+
 static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
 	.direction_get		= sit9531x_dpll_input_pin_direction_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,
 };
 
 /*
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index a892948da38d..da61a65ac2d2 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -135,6 +135,10 @@
 #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)
-- 
2.39.2 (Apple Git-143)


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

* [PATCH net-next v9 07/15] dpll: sit9531x: add support to get and set priority on input pins
  2026-09-15  0:00 [PATCH net-next v9 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (5 preceding siblings ...)
  2026-09-15  0:00 ` [PATCH net-next v9 06/15] dpll: sit9531x: implement input pin state on a DPLL Ali Rouhi
@ 2026-09-15  0:00 ` Ali Rouhi
  2026-09-17 15:01   ` netdev-bot+sashiko
  2026-09-15  0:00 ` [PATCH net-next v9 08/15] dpll: sit9531x: add support to get and set frequency on pins Ali Rouhi
                   ` (7 subsequent siblings)
  14 siblings, 1 reply; 33+ messages in thread
From: Ali Rouhi @ 2026-09-15  0:00 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel, Ali Rouhi

From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>

The priority table is ordered: the slot an input occupies is its
priority, and the device selects the lowest occupied slot whose signal is
qualified.  Getting the priority is therefore a search of the table for
the input, and setting it moves the input to the requested slot.

An input absent from the table has no priority to report, and the core is
told so rather than handed a made-up number.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/core.c |  76 +++++++++++++++++++++++---
 drivers/dpll/sit9531x/core.h |   8 +++
 drivers/dpll/sit9531x/dpll.c | 102 +++++++++++++++++++++++++++++++++++
 3 files changed, 178 insertions(+), 8 deletions(-)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 25418485e975..484c94def578 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -521,21 +521,81 @@ bool sit9531x_input_prio_present(struct sit9531x_dev *sitdev, u8 pll_idx,
 }
 
 /*
- * Rebuild a PLL's membership mask from the source codes of its priority
- * table.  The mask is what the pin state getters test, so it is refreshed
- * from exactly the values the table holds -- here after a write, and once
- * per poll from the read-back in sit9531x_chan_state_fetch().
+ * 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)
+ *
+ * Reports the last slot this source occupied on this PLL.  The value is
+ * cached from the hardware table read at startup and refreshed after every
+ * table write and poll read-back, so pin-get reflects hardware state without
+ * issuing synchronous register reads per pin.  A source with no known slot
+ * falls back to the lowest-priority valid slot.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_input_prio_get(struct sit9531x_dev *sitdev, u8 pll_idx,
+			    u8 input_idx, u8 *prio)
+{
+	const struct sit9531x_chan *chan;
+	u8 slot;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+	input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+	if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+		return -EINVAL;
+
+	chan = &sitdev->chan[pll_idx];
+	slot = chan->prio_last[input_idx];
+	if (!slot)
+		slot = SIT9531X_PRIO_MAX_SLOTS;
+
+	*prio = slot - 1;
+
+	return 0;
+}
+
+/*
+ * Refresh a PLL's cached view of its priority table from the source codes
+ * the table holds -- here after a write, and once per poll from the
+ * read-back in sit9531x_chan_state_fetch().
+ *
+ * The membership mask is what the pin state getters test; the per-slot
+ * copy and the last-slot-seen array are what priority get answers from,
+ * so neither costs a register read per pin.
  */
 static void sit9531x_prio_mask_build(struct sit9531x_dev *sitdev, u8 pll_idx,
 				     const u8 *srcs)
 {
+	struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+	u8 first[SIT9531X_PRIO_NUM_SRC] = { 0 };
 	u16 mask = 0;
-	u8 slot;
+	u8 slot, src, src_canon;
 
-	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++)
-		mask |= BIT(srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK);
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+		src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
+		chan->prio_srcs[slot] = src;
+		src_canon = sit9531x_prio_src_canon(sitdev, src);
+		if (src_canon >= SIT9531X_PRIO_NUM_SRC)
+			continue;
+
+		mask |= BIT(src_canon);
+		if (!first[src_canon])
+			first[src_canon] = slot + 1;
+	}
+
+	/*
+	 * Assign unconditionally: a source that has left the table has no
+	 * slot, and leaving its old one behind would keep reporting it as
+	 * listed for as long as the device runs.
+	 */
+	for (src = 0; src < SIT9531X_PRIO_NUM_SRC; src++)
+		chan->prio_last[src] = first[src];
 
-	sitdev->chan[pll_idx].prio_mask = mask;
+	chan->prio_mask = mask;
 }
 
 /* Attempts to release a forced holdover before reporting it stuck. */
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index f628bd5b7a97..1ab8ebfd78da 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -129,6 +129,12 @@ struct sit9531x_out {
  * @ho_freeze:		holdover freeze active
  * @ho_valid:		holdover memory acquired, i.e. the holdover window
  *			holds a valid estimate to fall back on
+ * @prio_srcs:		cached copy of the priority table, one source code
+ *			per slot; refreshed together with @prio_mask, so
+ *			priority reads generate no register traffic
+ * @prio_last:		slot each source occupies, plus one (0 = the source
+ *			is not in the table); refreshed from the same scan
+ *			as @prio_mask, so the two never disagree
  * @prio_mask:		bit per hardware source code present in this PLL's
  *			priority table, i.e. the sources it may select.  Read
  *			back from the table by the periodic worker and
@@ -143,6 +149,8 @@ struct sit9531x_chan {
 	bool		inner_lol;
 	bool		ho_freeze;
 	bool		ho_valid;
+	u8		prio_srcs[SIT9531X_PRIO_MAX_SLOTS];
+	u8		prio_last[SIT9531X_PRIO_NUM_SRC];
 	u16		prio_mask;
 };
 
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index ab9aa7db1839..e2ff21adfdb2 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -469,10 +469,112 @@ sit9531x_dpll_input_pin_state_on_dpll_set(const struct dpll_pin *pin,
 	return rc;
 }
 
+/*
+ * sit9531x_dpll_input_pin_prio_get - read input pin priority
+ *
+ * Reports the cached slot from sit9531x_input_prio_get().  The cache is
+ * refreshed from hardware at startup and by periodic read-back, so pin-get
+ * reports hardware priority without synchronous per-pin I2C reads.
+ */
+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 dpll_pin *changed[SIT9531X_MAX_INPUTS + 1];
+	struct sit9531x_dpll_pin *sibling;
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	u8 changed_cnt = 0, hw_src, slot;
+	int get_rc, 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);
+	if (!rc) {
+		list_for_each_entry(sibling, &sitdpll->pins, list) {
+			if (!sit9531x_dpll_is_input_pin(sibling) ||
+			    sit9531x_dpll_is_xo_pin(sibling))
+				continue;
+
+			hw_src = sit9531x_input_hw_src(sibling->id);
+			get_rc = sit9531x_input_prio_get(sitdev, sitdpll->id, hw_src, &slot);
+			if (get_rc)
+				continue;
+
+			if (sibling->prio == slot)
+				continue;
+
+			sibling->prio = slot;
+			if (changed_cnt < ARRAY_SIZE(changed))
+				changed[changed_cnt++] = sibling->dpll_pin;
+		}
+	}
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc == -EINVAL) {
+		NL_SET_ERR_MSG(extack,
+			       "Pin is not a reference of this DPLL; connect it first");
+		return rc;
+	}
+	if (rc) {
+		NL_SET_ERR_MSG(extack, "Failed to set input priority");
+		return rc;
+	}
+
+	while (changed_cnt--)
+		dpll_pin_change_ntf(changed[changed_cnt]);
+
+	return 0;
+}
+
 static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
 	.direction_get		= sit9531x_dpll_input_pin_direction_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,
 };
 
 /*
-- 
2.39.2 (Apple Git-143)


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

* [PATCH net-next v9 08/15] dpll: sit9531x: add support to get and set frequency on pins
  2026-09-15  0:00 [PATCH net-next v9 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (6 preceding siblings ...)
  2026-09-15  0:00 ` [PATCH net-next v9 07/15] dpll: sit9531x: add support to get and set priority on input pins Ali Rouhi
@ 2026-09-15  0:00 ` Ali Rouhi
  2026-09-17 15:01   ` netdev-bot+sashiko
  2026-09-15  0:00 ` [PATCH net-next v9 10/15] dpll: sit9531x: add support to adjust output phase Ali Rouhi
                   ` (6 subsequent siblings)
  14 siblings, 1 reply; 33+ messages in thread
From: Ali Rouhi @ 2026-09-15  0:00 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel, Ali Rouhi

From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>

Both directions in one patch, since they share everything that matters.

An input's frequency is what the board presents, so it is reported from
the firmware description rather than read back: the chip has no divider on
an input whose rate it merely qualifies.

An output's frequency is the VCO divided by that output's divider, so it
is computed from the divider read back from the chip and set by writing a
new one.  The VCO in turn comes from the feedback divider, which is why
the crystal rate is needed at probe.  A divider write only takes effect
inside the programming state, and that state has to be left with the
output loops re-locked whatever happened in between, so the exit runs even
when a write in the middle failed and the first error is the one returned.

Programming a divider costs about a hundred milliseconds under the device
lock: a dozen or so register transactions, then the settling time the part
requires after the loop-lock command, which is a property of the hardware
rather than a conservative guess.  The DPLL core holds its own lock across
the whole callback, so a frequency set on this device delays netlink
traffic for every DPLL in the system for that long.  Splitting the wait
out would need the ops to complete asynchronously, which the interface
does not offer; issuing the commit without waiting would let the next
request program a part that has not settled.  A rate change is a
configuration action, not something a running system does per packet, so
the cost is paid where it is visible rather than hidden behind a
completion the caller cannot wait for.

The phase flush that follows a divider write realigns every output fed by
that PLL, not only the one that changed.  The flush is a per-PLL function
in the device and there is no per-output equivalent, so an output whose
rate is set while its siblings are running will step their phase too.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/core.c | 587 +++++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/dpll.c |  94 ++++++
 drivers/dpll/sit9531x/regs.h |  17 +
 3 files changed, 698 insertions(+)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 484c94def578..a83bf8ac83c1 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -396,6 +396,84 @@ static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev,
 	return 0;
 }
 
+/*
+ * 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);
+}
+
+/* Attempts to re-lock the output loops before reporting them open. */
+#define SIT9531X_LOOP_LOCK_TRIES	3
+
+/*
+ * 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, rc2 = 0, rc3;
+	u8 attempt;
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+			       SIT9531X_UPDATE_NVM);
+
+	/*
+	 * Issue the loop lock even if the update failed.  Callers reach
+	 * this function through a goto so that the chip never stays in
+	 * the PRG_CMD state with its loops open; returning early here
+	 * would defeat that and leave the outputs unlocked until the
+	 * next successful commit.
+	 */
+	/*
+	 * Re-lock the loops.  Leaving them open is worse than any other
+	 * failure this function can report, and nothing else closes them,
+	 * so retry as the priority table does with its own latch.
+	 */
+	for (attempt = 0; attempt < SIT9531X_LOOP_LOCK_TRIES; attempt++) {
+		rc2 = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+					SIT9531X_LOOP_LOCK);
+		if (!rc2)
+			break;
+		usleep_range(1000, 2000);
+	}
+	if (rc2)
+		dev_err(sitdev->dev,
+			"output loops left unlocked after programming: %d\n",
+			rc2);
+
+	msleep(100);
+
+	/*
+	 * Put the output-system debug block back the way the device powers
+	 * up.  Its key register unlocks every debug register while it holds
+	 * the unlock value, and each programming sequence writes that value
+	 * itself, so nothing needs it left unlocked in between.
+	 */
+	rc3 = sit9531x_write_u8(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
+				SIT9531X_DEBUG_LOCK_VAL);
+
+	if (rc)
+		return rc;
+
+	return rc2 ? rc2 : rc3;
+}
+
 /*
  * Input priority selection
  *
@@ -908,6 +986,11 @@ int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_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 */
@@ -921,6 +1004,510 @@ int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
 /* The output divider is a 34-bit field */
 #define SIT9531X_DIVO_MAX			GENMASK_ULL(33, 0)
 
+/*
+ * 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;
+}
+
+/*
+ * 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_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.
+ *
+ * Return: 0 with *fvco set on success, -ENODATA when DIVN is not
+ * programmed (dormant PLL), or the register access error.  A bus
+ * failure is never folded into the -ENODATA case, so callers can fail
+ * a request instead of acting on a guessed rate.
+ */
+static int sit9531x_get_fvco(struct sit9531x_dev *sitdev, u8 pll_idx,
+			     u64 *fvco)
+{
+	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]) {
+		*fvco = sitdev->pll_fvco[pll_idx];
+		return 0;
+	}
+
+	rc = sit9531x_divn_static(sitdev, pll_idx, &divn);
+	if (rc)
+		return rc;
+	if (divn <= 0)
+		return -ENODATA;
+
+	doubler = sit9531x_is_xo_doubler_enabled(sitdev);
+	if (doubler < 0)
+		return doubler;
+
+	fref = (u64)sitdev->xtal_freq << doubler;
+
+	*fvco = mul_u64_u64_div_u64(fref, (u64)divn, SIT9531X_DIVN_SCALE);
+
+	/*
+	 * A DIVN of less than one whole cycle passes the check above and
+	 * still truncates the product to zero.  Callers divide by this, so
+	 * report the unprogrammed divider it describes rather than handing
+	 * back a zero denominator.
+	 */
+	if (!*fvco)
+		return -ENODATA;
+
+	return 0;
+}
+
+/*
+ * 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 documented 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)
+		goto disarm;
+
+	/*
+	 * 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)
+		goto disarm;
+
+	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)
+		goto disarm;
+
+	/*
+	 * 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;
+
+disarm:
+	/*
+	 * Disarm the on-demand flush enable armed above.  Leaving it set
+	 * would let a later assertion of the restored trigger source
+	 * re-flush every output divider of this PLL, which is exactly the
+	 * persistent side effect the one-shot sequence must not have.
+	 */
+	ret = sit9531x_update_pll_u8(sitdev, pll_idx,
+				     SIT9531X_PLL_REG_PHFL_CTRL,
+				     SIT9531X_PLL_PHFL_ON_DEMAND_EN, 0);
+	if (!ret)
+		ret = sit9531x_update_pll_u8(sitdev, pll_idx,
+					     SIT9531X_PLL_REG_SMALL_UPDATE,
+					     SIT9531X_SMALL_UPDATE_CMD,
+					     SIT9531X_SMALL_UPDATE_CMD);
+	if (ret && !rc)
+		rc = ret;
+
+	return rc;
+}
+
+/*
+ * sit9531x_output_divo_calc - work out an output's divider and its VCO
+ *
+ * Separated from the write so a caller that programs more than the
+ * divider in one sequence can compute the value before it enters the
+ * programming state.
+ */
+static int sit9531x_output_divo_calc(struct sit9531x_dev *sitdev, u8 out_idx,
+				     u8 pll_idx, u64 frequency, u64 *fvco_out,
+				     u64 *divo_out)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	u64 fvco, divo, fvco_min, fvco_max;
+	int rc;
+
+	if (out_idx >= info->num_outputs || pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	if (!frequency)
+		return -EINVAL;
+
+	/*
+	 * The core validates the request against the supported ranges with
+	 * the value narrowed to u32 but hands the full u64 down, so a value
+	 * like U32_MAX + 1 Hz validates as 1 Hz.  Reject anything that does
+	 * not fit the narrowed width the validation actually covered.
+	 */
+	if (frequency > U32_MAX)
+		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.  A rate read from the device
+	 * is clamped rather than refused: the bands bound what the VCO can
+	 * physically run at, the readback is the only estimate available,
+	 * and refusing would make every output unprogrammable on a part
+	 * whose DIVN registers do not describe the running VCO.  A DT
+	 * override is refused instead, at parse time, because there the
+	 * board is asserting a value it should know.  A VCO that cannot be read fails
+	 * the request: programming a divider from a guessed rate would put
+	 * the output far from what was asked for while reporting success.
+	 */
+	rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
+	if (rc)
+		return rc == -ENODATA ? -ENODEV : rc;
+	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;
+
+	/*
+	 * DIVO is a 34-bit field.  With a band-clamped Fvco this cannot
+	 * overflow, but a DT Fvco override is taken verbatim, so guard the
+	 * field width rather than silently truncating the divider.
+	 */
+	if (divo > SIT9531X_DIVO_MAX)
+		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));
+
+	*fvco_out = fvco;
+	*divo_out = divo;
+
+	return 0;
+}
+
+/*
+ * sit9531x_output_divo_write - write the five DIVO bytes of an output
+ *
+ * The caller must already be in the programming state.  Bytes written
+ * before a failure are put back, so the output keeps the divider it had
+ * rather than a mixture of the two.
+ */
+static int sit9531x_output_divo_write(struct sit9531x_dev *sitdev, u8 out_idx,
+				      u64 divo)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	u8 slot, page, base_reg, divo_bytes[5], old_bytes[5], msb_old;
+	int rc, j, rb_rc;
+	u8 written = 0;
+
+	/* 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];
+
+	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] */
+
+	for (j = 0; j < 5; j++) {
+		rc = sit9531x_read_u8(sitdev,
+				      SIT9531X_REG(page, base_reg - j),
+				      &old_bytes[j]);
+		if (rc)
+			return rc;
+	}
+
+	msb_old = old_bytes[4];
+	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 rollback;
+		written++;
+	}
+
+	return 0;
+
+rollback:
+	for (j = 0; j < written; j++) {
+		rb_rc = sit9531x_write_u8(sitdev,
+					  SIT9531X_REG(page, base_reg - j),
+					  old_bytes[j]);
+		if (rb_rc) {
+			dev_err(sitdev->dev,
+				"out%u: DIVO rollback failed (%d), the divider is part old and part new\n",
+				out_idx, rb_rc);
+			if (!rc)
+				rc = rb_rc;
+		}
+	}
+
+	return rc;
+}
+
+int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
+			     u8 pll_idx, u64 frequency)
+{
+	u64 fvco, divo;
+	int rc, ret;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	rc = sit9531x_output_divo_calc(sitdev, out_idx, pll_idx, frequency,
+				       &fvco, &divo);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_prg_enter(sitdev);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_output_divo_write(sitdev, out_idx, divo);
+	/*
+	 * 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 = 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;
+
+	rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
+	if (rc)
+		return rc == -ENODATA ? -ENODEV : rc;
+
+	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 = *frequency;
+
+	return 0;
+}
+
 /*
  * Phase adjust (PRG_RST_DELAY register-based).
  *
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index e2ff21adfdb2..88122a368053 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -335,6 +335,28 @@ sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
 	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
  *
@@ -571,6 +593,7 @@ sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
 
 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,
@@ -629,6 +652,7 @@ sit9531x_dpll_xo_pin_state_on_dpll_get(const struct dpll_pin *pin,
 
 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,
 };
 
@@ -644,8 +668,78 @@ sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
 	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 only when
+ * the output is not resolvable through the divider chain (e.g. not
+ * mapped to a PLL), so transport/register errors still surface.
+ */
+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);
+	if (rc == -ENODEV)
+		*frequency = sit9531x_out_state_get(sitdev, dpin->id)->freq;
+	mutex_unlock(&sitdev->multiop_lock);
+
+	return rc == -ENODEV ? 0 : rc;
+}
+
+/*
+ * 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;
+}
+
 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,
 };
 
 const struct dpll_pin_ops *
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index da61a65ac2d2..058b892d656e 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -201,6 +201,7 @@
 /* Debug register (same offset, per-page) */
 #define SIT9531X_REG_OUTSYS_DEBUG		SIT9531X_REG(0x03, 0xBD)
 #define SIT9531X_DEBUG_UNLOCK_VAL		0xC3
+#define SIT9531X_DEBUG_LOCK_VAL			0x00
 
 /*
  * On-demand phase-flush fired from a register rather than a GPIO pin.
@@ -215,6 +216,10 @@
 /* ---- 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
@@ -227,6 +232,18 @@
 #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) */
+
+/*
+ * 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
+
 #define SIT9531X_PLL_REG_ACTIVE		0x02
 #define SIT9531X_PLL_ACTIVE_BIT		BIT(0)  /* PLL reached active state */
 #define SIT9531X_PLL_REG_ZDB0		0x2B
-- 
2.39.2 (Apple Git-143)


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

* [PATCH net-next v9 10/15] dpll: sit9531x: add support to adjust output phase
  2026-09-15  0:00 [PATCH net-next v9 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (7 preceding siblings ...)
  2026-09-15  0:00 ` [PATCH net-next v9 08/15] dpll: sit9531x: add support to get and set frequency on pins Ali Rouhi
@ 2026-09-15  0:00 ` Ali Rouhi
  2026-09-17  9:55   ` Ivan Vecera
  2026-09-17 15:01   ` netdev-bot+sashiko
  2026-09-15  0:00 ` [PATCH net-next v9 09/15] dpll: sit9531x: implement output pin state on a DPLL Ali Rouhi
                   ` (5 subsequent siblings)
  14 siblings, 2 replies; 33+ messages in thread
From: Ali Rouhi @ 2026-09-15  0:00 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel, Ali Rouhi

From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>

Shift an output in time against the others driven by the same PLL.  The
device has a coarse delay counted in VCO cycles and a three-bit fine field
in fixed thirty-picosecond steps, so a requested offset is split between
the two and what the core reads back is what the registers hold rather
than what was asked for.

Delay only ever advances, so an offset larger than one output period is
folded back into a single period -- for a periodic signal that is the same
phase.  The write takes effect in the programming state, which is left
with the loops re-locked even when a write inside it failed.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/core.c | 212 ++++++++++++++++++++++++++++++++++-
 drivers/dpll/sit9531x/core.h |   4 +
 drivers/dpll/sit9531x/dpll.c |  56 +++++++++
 drivers/dpll/sit9531x/regs.h |  27 +++++
 4 files changed, 298 insertions(+), 1 deletion(-)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index bba42fe302a3..8d857f1a0c89 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -1641,7 +1641,17 @@ int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
 
 	sitdev->out[out_idx].freq = div64_u64(fvco, divo);
 
-	return 0;
+	/*
+	 * The programmed reset delay counts VCO cycles against the output
+	 * period in force when it was written, so a rate change silently
+	 * re-times a previously requested phase adjust.  Re-encode the
+	 * cached picosecond request against the new rate.
+	 */
+	if (sitdev->out[out_idx].phase_adj)
+		rc = sit9531x_output_phase_adjust_set(sitdev, out_idx,
+						      sitdev->out[out_idx].phase_adj);
+
+	return rc;
 }
 
 /*
@@ -1731,6 +1741,206 @@ int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
  * 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, t_out_ps;
+	s64 phase_norm_ps = 0;
+	u8 page, base, prog6_val, fine = 0;
+	u8 old_bytes[5], new_bytes[5], i;
+	u8 pll_idx, slot;
+	u64 freq;
+	int rc, ret, rb_rc;
+
+	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 -EINVAL;
+
+	freq = sitdev->out[out_idx].freq;
+	if (!freq) {
+		/*
+		 * The cache is only seeded by a DT frequency list or an
+		 * earlier get/set; a board without supported-frequencies-hz
+		 * would otherwise get -EINVAL on every phase request forever.
+		 * Read the effective rate back from the divider chain.
+		 */
+		rc = sit9531x_output_freq_get(sitdev, out_idx, &freq);
+		if (rc)
+			return rc;
+		if (!freq)
+			return -EINVAL;
+	}
+
+	rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
+	if (rc)
+		return rc == -ENODATA ? -ENODEV : rc;
+
+	t_out_ps = div64_u64(1000000000000ULL, freq);
+	if (!t_out_ps)
+		return -EINVAL;
+
+	/*
+	 * Convert to unsigned absolute delay.  Both signs are folded modulo one
+	 * period: positive delays wrap naturally, negative delays are rendered as
+	 * T_out - |phase|.
+	 */
+	if (phase_ps == 0) {
+		abs_ps = 0;
+	} else if (phase_ps > 0) {
+		abs_ps = (u64)phase_ps;
+		div64_u64_rem(abs_ps, t_out_ps, &abs_ps);
+		phase_norm_ps = abs_ps;
+	} else {
+		u64 advance = (u64)(-(s64)phase_ps);
+
+		/*
+		 * div64_u64_rem() rather than the % operator: a 64-bit
+		 * modulo has no compiler helper on 32-bit targets and
+		 * leaves the module with an undefined __umoddi3.
+		 */
+		div64_u64_rem(advance, t_out_ps, &advance);
+		phase_norm_ps = -(s64)advance;
+		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);
+
+	/*
+	 * 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;
+
+	for (i = 0; i < ARRAY_SIZE(old_bytes); i++) {
+		rc = sit9531x_read_u8(sitdev, SIT9531X_REG(page, base + i),
+				      &old_bytes[i]);
+		if (rc)
+			goto commit;
+	}
+
+	/* PROG6 RMW: preserve OPSTG_VCASC_BUMP in [7:5] */
+	prog6_val = old_bytes[0] & 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);
+
+	new_bytes[0] = prog6_val;
+	new_bytes[1] = (u8)((coarse >> 24) & 0xFF);
+	new_bytes[2] = (u8)((coarse >> 16) & 0xFF);
+	new_bytes[3] = (u8)((coarse >> 8) & 0xFF);
+	new_bytes[4] = (u8)(coarse & 0xFF);
+
+	for (i = 0; i < ARRAY_SIZE(new_bytes); i++) {
+		rc = sit9531x_write_u8(sitdev,
+				       SIT9531X_REG(page, base + i),
+				       new_bytes[i]);
+		if (rc)
+			goto rollback;
+	}
+
+	goto commit;
+
+rollback:
+	rb_rc = 0;
+	for (i = 0; i < ARRAY_SIZE(old_bytes); i++) {
+		ret = sit9531x_write_u8(sitdev,
+					SIT9531X_REG(page, base + i),
+					old_bytes[i]);
+		if (ret && !rb_rc)
+			rb_rc = ret;
+	}
+	if (rb_rc) {
+		dev_err(sitdev->dev,
+			"out%u: phase-adjust rollback failed (%d), the delay registers are part old and part new\n",
+			out_idx, rb_rc);
+		if (!rc)
+			rc = rb_rc;
+	}
+
+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.
+	 */
+	rc = sit9531x_output_phase_flush(sitdev, pll_idx);
+	if (rc)
+		return rc;
+
+	/*
+	 * Cache what the registers realize, and only once every step has
+	 * succeeded: the core drops a repeated request with the same value,
+	 * so a cache updated by a failed call would make the retry a no-op.
+	 *
+	 * Quantizing to whole VCO cycles plus 30 ps steps can land a few
+	 * picoseconds past the end of the period, which would wrap the
+	 * subtraction below; one period is the most a delay can be.
+	 */
+	coarse_ps = mul_u64_u64_div_u64(coarse, 1000000000000ULL, fvco);
+	abs_ps = coarse_ps + (u64)fine * SIT9531X_OUT_PRG_FINE_STEP_PS;
+	if (abs_ps > t_out_ps)
+		abs_ps = t_out_ps;
+	if (phase_norm_ps < 0)
+		sitdev->out[out_idx].phase_adj =
+			abs_ps ? -(s32)(t_out_ps - abs_ps) : 0;
+	else
+		sitdev->out[out_idx].phase_adj = (s32)abs_ps;
+
+	return 0;
+}
+
 /*
  * sit9531x_clear_notifications - clear all notification registers
  *
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index db9c73a79b04..06df7a91a7c6 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -105,6 +105,9 @@ struct sit9531x_ref {
  * @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)
+ * @phase_adj:		phase adjust the delay registers actually realize,
+ *			i.e. the last request quantized to whole VCO cycles
+ *			plus 30 ps fine steps, in the request's sign
  * @label:		board label from DT or default
  */
 struct sit9531x_out {
@@ -114,6 +117,7 @@ struct sit9531x_out {
 	bool		state_stale;
 	bool		routed;
 	u8		pll_idx;
+	s32		phase_adj;
 	const char	*label;
 };
 
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index d550eb626e6d..78034b7f089c 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -815,12 +815,68 @@ sit9531x_dpll_output_pin_state_on_dpll_set(const struct dpll_pin *pin,
 	return rc;
 }
 
+/*
+ * sit9531x_dpll_output_pin_phase_adjust_get - read output phase adjustment
+ *
+ * Returns the applied phase cached in out->phase_adj, i.e. the value
+ * sit9531x_output_phase_adjust_set() actually programmed after quantization.
+ */
+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;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+
+	mutex_lock(&sitdev->multiop_lock);
+	*phase_adjust = sit9531x_out_state_get(sitdev, dpin->id)->phase_adj;
+	mutex_unlock(&sitdev->multiop_lock);
+
+	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;
+	}
+
+	return 0;
+}
+
 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,
 };
 
 const struct dpll_pin_ops *
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index 058b892d656e..f40762f06403 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -203,6 +203,33 @@
 #define SIT9531X_DEBUG_UNLOCK_VAL		0xC3
 #define SIT9531X_DEBUG_LOCK_VAL			0x00
 
+/*
+ * 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]
+ *
+ * Slots 0-5 are on Page 3, slots 6-11 on Page 4.  The block base
+ * within a page is 0x15 + 16 * (slot % 6), where slot is the physical
+ * output slot from clkout_map[], not the logical output index.
+ */
+#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
+
 /*
  * On-demand phase-flush fired from a register rather than a GPIO pin.
  * DIVO_PHASE_SEL_REG selects the in-register trigger source and
-- 
2.39.2 (Apple Git-143)


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

* [PATCH net-next v9 09/15] dpll: sit9531x: implement output pin state on a DPLL
  2026-09-15  0:00 [PATCH net-next v9 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (8 preceding siblings ...)
  2026-09-15  0:00 ` [PATCH net-next v9 10/15] dpll: sit9531x: add support to adjust output phase Ali Rouhi
@ 2026-09-15  0:00 ` Ali Rouhi
  2026-09-17 15:01   ` netdev-bot+sashiko
  2026-09-15  0:00 ` [PATCH net-next v9 12/15] dpll: sit9531x: add support to get phase offset on the connected input pin Ali Rouhi
                   ` (4 subsequent siblings)
  14 siblings, 1 reply; 33+ messages in thread
From: Ali Rouhi @ 2026-09-15  0:00 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel, Ali Rouhi

From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>

An output is driven by its PLL rather than selected by it, so its state
says whether it carries a signal: connected while it drives, disconnected
while it is muted.  Setting the state mutes or un-mutes it by forcing the
pad to Hi-Z, the only per-output control the device offers that leaves the
divider alone.

The force bit and the state bit are separate, and a pad follows the loaded
configuration while the force bit is clear, so both are read to decide
whether an output is muted and both are written to change it.  Which of
the four register banks applies depends on the slot and on whether the pad
is single-ended or differential.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/core.c | 221 ++++++++++++++++++++++++++++++++++-
 drivers/dpll/sit9531x/core.h |   5 +
 drivers/dpll/sit9531x/dpll.c |  81 +++++++++++++
 3 files changed, 303 insertions(+), 4 deletions(-)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index a83bf8ac83c1..bba42fe302a3 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -396,6 +396,47 @@ static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev,
 	return 0;
 }
 
+/*
+ * sit9531x_output_state_refresh - read an output's mute state back
+ *
+ * Used when a mute could not be confirmed at the time it was written.  The
+ * driver does not poll output state, so without this the cached value would
+ * stand until something else happened to write it.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_output_state_refresh(struct sit9531x_dev *sitdev, u8 out_idx)
+{
+	bool muted;
+	int rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	rc = sit9531x_output_forced_hiz(sitdev, out_idx, &muted);
+	if (rc)
+		return rc;
+
+	sitdev->out[out_idx].enabled = !muted;
+	sitdev->out[out_idx].state_stale = false;
+
+	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
@@ -474,6 +515,166 @@ static int sit9531x_prg_commit(struct sit9531x_dev *sitdev)
 	return rc2 ? rc2 : rc3;
 }
 
+/*
+ * sit9531x_output_hiz_write - mute or unmute an output
+ *
+ * Muting takes control of the pin (MASK=1) and drives it low (STATE=0) on
+ * both the differential and the single-ended register pair, because the
+ * output must go quiet whichever way it is wired; unmuting hands it back
+ * to the device's own state machine.  The caller must already be in the
+ * programming state.
+ */
+static int sit9531x_output_hiz_write(struct sit9531x_dev *sitdev, u8 slot,
+				     bool mute)
+{
+	struct sit9531x_hiz_regs r;
+	int rc;
+
+	sit9531x_output_get_hiz_regs(slot, &r);
+
+	if (!mute) {
+		rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, false);
+		if (rc)
+			return rc;
+
+		return sit9531x_hiz_set_bit(sitdev, r.se_mask, r.bit, false);
+	}
+
+	rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, true);
+	if (rc)
+		return rc;
+	rc = sit9531x_hiz_set_bit(sitdev, r.diff_state, r.bit, false);
+	if (rc)
+		return rc;
+	rc = sit9531x_hiz_set_bit(sitdev, r.se_mask, r.bit, true);
+	if (rc)
+		return rc;
+
+	return sit9531x_hiz_set_bit(sitdev, r.se_state, r.bit, false);
+}
+
+/*
+ * sit9531x_output_disable - mute an output (force Hi-Z)
+ * @index:	logical output index (0..info->num_outputs-1)
+ *
+ * Sets MASK and clears 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;
+	bool muted;
+	u8 slot;
+	int rc, ret, state_rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (index >= info->num_outputs)
+		return -EINVAL;
+
+	slot = info->clkout_map[index];
+	rc = sit9531x_prg_enter(sitdev);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_output_hiz_write(sitdev, slot, true);
+
+	/*
+	 * 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;
+
+	/*
+	 * Keep the software state aligned to what hardware now drives even
+	 * when one write in the sequence failed. The commit above may have
+	 * applied a partial mask/state combination.
+	 */
+	state_rc = sit9531x_output_forced_hiz(sitdev, index, &muted);
+	if (!state_rc) {
+		sitdev->out[index].enabled = !muted;
+		sitdev->out[index].state_stale = false;
+	} else {
+		/*
+		 * The writes may well have landed; what failed is the proof.
+		 * Mark the cached state for a read-through rather than
+		 * reporting the value it had before this call.
+		 */
+		sitdev->out[index].state_stale = true;
+		if (!rc)
+			rc = state_rc;
+	}
+
+	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;
+	bool muted;
+	u8 slot;
+	int rc, ret, state_rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (index >= info->num_outputs)
+		return -EINVAL;
+
+	slot = info->clkout_map[index];
+	rc = sit9531x_prg_enter(sitdev);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_output_hiz_write(sitdev, slot, false);
+
+	/*
+	 * 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;
+
+	/* See sit9531x_output_disable(): commit can apply a partial sequence. */
+	state_rc = sit9531x_output_forced_hiz(sitdev, index, &muted);
+	if (!state_rc) {
+		sitdev->out[index].enabled = !muted;
+		sitdev->out[index].state_stale = false;
+	} else {
+		/*
+		 * The writes may well have landed; what failed is the proof.
+		 * Mark the cached state for a read-through rather than
+		 * reporting the value it had before this call.
+		 */
+		sitdev->out[index].state_stale = true;
+		if (!rc)
+			rc = state_rc;
+	}
+
+	return rc;
+}
+
 /*
  * Input priority selection
  *
@@ -1815,6 +2016,8 @@ static int sit9531x_out_state_fetch(struct sit9531x_dev *sitdev, u8 index)
 	if (rc)
 		return rc;
 
+	sitdev->out[index].state_stale = false;
+
 	/*
 	 * DT board-config override: the per-PLL OUTPUT_ENABLE bitmaps
 	 * (0x27/0x28) do not unambiguously express output->PLL routing on
@@ -2455,13 +2658,23 @@ static bool sit9531x_dpll_pin_is_registrable(struct sit9531x_dpll *sitdpll,
 {
 	struct sit9531x_dev *sitdev = sitdpll->dev;
 
-	if (dir != DPLL_PIN_DIRECTION_INPUT)
+	if (dir == DPLL_PIN_DIRECTION_INPUT) {
+		if (index == SIT9531X_MAX_INPUTS)
+			return true;
+		if (index == SIT9531X_INTSYNC_PIN_ID)
+			return false;
+
+		return sit9531x_input_pin_is_registrable(sitdev, index);
+	}
+
+	if (index == SIT9531X_INTSYNC_OUT_PIN_ID)
 		return false;
 
-	if (index == SIT9531X_MAX_INPUTS)
-		return true;
+	if (index >= sitdev->info->num_outputs)
+		return false;
 
-	return sit9531x_input_pin_is_registrable(sitdev, index);
+	return sitdev->out[index].pll_idx == sitdpll->id &&
+	       sitdev->out[index].routed;
 }
 
 /*
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 1ab8ebfd78da..db9c73a79b04 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -100,6 +100,8 @@ struct sit9531x_ref {
  * @enabled:		output is driving, i.e. not forced into Hi-Z
  * @cmos:		output is wired single-ended; the Hi-Z pair that
  *			speaks for it is the SE one, not the differential
+ * @state_stale:	the cached mute state could not be confirmed against
+ *			hardware and has to be read back before it is reported
  * @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)
@@ -109,6 +111,7 @@ struct sit9531x_out {
 	u64		freq;
 	bool		enabled;
 	bool		cmos;
+	bool		state_stale;
 	bool		routed;
 	u8		pll_idx;
 	const char	*label;
@@ -253,6 +256,8 @@ int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_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);
+int sit9531x_output_state_refresh(struct sit9531x_dev *sitdev,
+				  u8 out_idx);
 
 /* ---- Output frequency ---- */
 int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index 88122a368053..d550eb626e6d 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -736,10 +736,91 @@ sit9531x_dpll_output_pin_frequency_set(const struct dpll_pin *pin,
 	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;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	const struct sit9531x_out *out;
+
+	/*
+	 * A mute whose read-back failed left the cache unconfirmed; there is
+	 * no poll of output state to correct it, so read it here rather than
+	 * report a value that may predate the request.
+	 */
+	if (sitdev->out[dpin->id].state_stale) {
+		mutex_lock(&sitdev->multiop_lock);
+		sit9531x_output_state_refresh(sitdev, dpin->id);
+		mutex_unlock(&sitdev->multiop_lock);
+	}
+
+	out = sit9531x_out_state_get(sitdev, 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;
+}
+
 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,
 };
 
 const struct dpll_pin_ops *
-- 
2.39.2 (Apple Git-143)


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

* [PATCH net-next v9 12/15] dpll: sit9531x: add support to get phase offset on the connected input pin
  2026-09-15  0:00 [PATCH net-next v9 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (9 preceding siblings ...)
  2026-09-15  0:00 ` [PATCH net-next v9 09/15] dpll: sit9531x: implement output pin state on a DPLL Ali Rouhi
@ 2026-09-15  0:00 ` Ali Rouhi
  2026-09-17 15:01   ` netdev-bot+sashiko
  2026-09-15  0:00 ` [PATCH net-next v9 11/15] dpll: sit9531x: add support to get and set esync on pins Ali Rouhi
                   ` (3 subsequent siblings)
  14 siblings, 1 reply; 33+ messages in thread
From: Ali Rouhi @ 2026-09-15  0:00 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel, Ali Rouhi

From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>

Report the phase difference between a PLL's reference and the PLL itself,
which is the loop's own residual error and therefore trends small on a
locked device -- that is the measurement, not an artefact of it.

The value comes from the on-chip time-to-digital converter, read through
the debug window: unlock the window, point it at the converter, then read
the trigger register, which latches a fresh sample and returns the
previous one.  It is read three times per sample for that reason; a single
read hands back the sample from the last call, so a repeated measurement
would look perfectly steady while saying nothing.

Only the input a PLL has actually selected has a phase offset against it.
For any other pin there is nothing to measure and zero is reported,
because the core abandons an entire pin dump on an error from any one pin.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/core.c | 148 +++++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/dpll.c | 102 ++++++++++++++++++++++++
 drivers/dpll/sit9531x/regs.h |  38 +++++++++
 3 files changed, 288 insertions(+)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 46179a9e13c3..356d1eac4ec5 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -2132,6 +2132,154 @@ int sit9531x_output_pulse_ctrl_set(struct sit9531x_dev *sitdev,
 	return rc;
 }
 
+/*
+ * sit9531x_phase_offset_read - read phase difference via TDC
+ * @phase_ps:	output phase difference in picoseconds
+ *
+ * Reads the Time-to-Digital Converter (TDC) signed 35-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, mag_ps;
+	s64 tdc_signed;
+	u64 tdc_raw;
+	int rc, lock_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)
+		goto relock;
+
+	/*
+	 * 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)
+		goto relock;
+	rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+				   SIT9531X_PLL_REG_DBG_READ_CODE,
+				   SIT9531X_DBG_READ_CODE_TDC);
+	if (rc)
+		goto relock;
+
+	/*
+	 * Latch a sample by reading the trigger register.  A single
+	 * read returns the previous latch, so read it three times as
+	 * the documented phase-difference procedure does.
+	 */
+	for (i = 0; i < SIT9531X_DBG_LATCH_READS; i++) {
+		rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+					  SIT9531X_PLL_REG_DBG_TRIGGER, &v);
+		if (rc)
+			goto relock;
+	}
+
+	tdc_raw = 0;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_4, &v);
+	if (rc)
+		goto relock;
+	sign = !!(v & BIT(SIT9531X_TDC_SIGN_BIT));
+	tdc_raw = (u64)(v & SIT9531X_TDC_MAG_HI_MASK) << 32;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_3, &v);
+	if (rc)
+		goto relock;
+	tdc_raw |= (u64)v << 24;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_2, &v);
+	if (rc)
+		goto relock;
+	tdc_raw |= (u64)v << 16;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_1, &v);
+	if (rc)
+		goto relock;
+	tdc_raw |= (u64)v << 8;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_0, &v);
+	if (rc)
+		goto relock;
+	tdc_raw |= v;
+
+	/*
+	 * Apply sign.  Per the register map 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.  -ENODATA means DIVN is not
+	 * programmed (PLL unused on this board) -- skip silently rather
+	 * than spamming the log on every poll cycle.  A register access
+	 * failure is returned as an error, not folded into a fabricated
+	 * 0 ps measurement.
+	 */
+	rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
+	if (rc == -ENODATA) {
+		dev_dbg(sitdev->dev, "PLL%c: Fvco unknown, skip TDC\n",
+			'A' + pll_idx);
+		rc = -ENODEV;
+		goto relock;
+	}
+	if (rc)
+		goto relock;
+
+	/*
+	 * phase_diff (seconds) = tdc_code / fvco
+	 * phase_diff (ps) = tdc_code * 1e12 / fvco
+	 *
+	 * mul_u64_u64_div_u64() keeps the exact Hz denominator; dividing
+	 * by whole MHz instead would lose up to ~40 ppm of scale on a
+	 * fractional-DIVN Fvco.
+	 */
+	mag_ps = mul_u64_u64_div_u64(tdc_signed < 0 ? -tdc_signed : tdc_signed,
+				     1000000000000ULL, fvco);
+	*phase_ps = tdc_signed < 0 ? -(s64)mag_ps : (s64)mag_ps;
+
+	rc = 0;
+
+relock:
+	/*
+	 * Close the debug window again.  The key register opens every debug
+	 * register on this PLL while it holds the unlock value, and this read
+	 * runs on every pin-get of a connected input, so leaving it open
+	 * would mean normal monitoring permanently unlocks the block.
+	 */
+	lock_rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+					SIT9531X_PLL_REG_DEBUG,
+					SIT9531X_PLL_DEBUG_LOCK);
+	if (lock_rc && !rc)
+		rc = lock_rc;
+
+	return rc;
+}
+
 /*
  * sit9531x_ref_state_fetch - read input reference status from hardware
  * @index:	logical input index
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index 0cad081eb599..1dfd83b635e2 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -603,6 +603,107 @@ sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
 	return 0;
 }
 
+/*
+ * sit9531x_dpll_input_pin_phase_offset_get - phase offset of a reference
+ *
+ * What this reports, and what it deliberately does not:
+ *
+ * The ABI defines the attribute as the phase difference between the signal
+ * on a pin and its parent DPLL device, so this is the loop's own residual
+ * error, sampled with the loop closed.  On a locked DPLL it therefore
+ * trends small -- that is the measurement, not an artefact of it.  The
+ * framework expects successive values to be averaged, which suits a
+ * closed-loop residual and not a one-shot open-loop capture.
+ *
+ * The chip can also measure the reference against the local oscillator
+ * with the outer loop's correction frozen, which is a different quantity
+ * and the one the documented phase-difference procedure produces.  That
+ * needs the digital loop filter held (and, on the 1PPS PLL, the automatic
+ * phase- and frequency-lock helpers held off), which leaves the PLL
+ * undisciplined until it is released.  A netlink read must not do that,
+ * so the open-loop measurement lives in a debugfs helper that owns the
+ * freeze and restores it; it is not this callback.
+ *
+ * Precondition, which this callback cannot create: the TDC compares
+ * against a signal the PLL drives, so a PLL driving no output with its
+ * zero-delay buffer off has nothing to measure.  SiTime confirms this is
+ * a property of the hardware rather than of their measurement script.
+ * The script satisfies it by mapping a spare output and restarting the
+ * PLL -- side effects that do not belong in a getter, so a reading taken
+ * in that state is simply not meaningful.
+ *
+ * Non-selected pins and a PLL with no programmed divider report zero
+ * rather than an error: the DPLL core propagates any error from this
+ * callback and fails the whole pin dump with it, unlike the frequency
+ * offset getter, where -ENODATA makes the core omit the attribute.  There
+ * is no per-pin "no data" for phase offset, so it is a value or no
+ * callback at all.
+ */
+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;
+	enum dpll_pin_state state;
+	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.
+	 */
+	sit9531x_dpll_selection_state_get(sitdev, sitdpll, dpin->id, &state);
+	if (state != DPLL_PIN_STATE_CONNECTED) {
+		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,
@@ -610,6 +711,7 @@ static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
 	.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_offset_get	= sit9531x_dpll_input_pin_phase_offset_get,
 };
 
 /*
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index 9d515e109feb..08eae8a2c279 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -271,6 +271,44 @@
 #define SIT9531X_PLL_REG_DIVN_NUM		0x32  /* 4 bytes (0x32-0x35) */
 #define SIT9531X_PLL_REG_DIVN_DEN		0x38  /* 4 bytes (0x38-0x3B) */
 
+/* Debug register unlock */
+#define SIT9531X_PLL_REG_DEBUG		0xBD
+#define SIT9531X_PLL_DEBUG_UNLOCK		0xC3
+#define SIT9531X_PLL_DEBUG_LOCK			0x00
+
+/*
+ * 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  /* [34: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 */
+
+/*
+ * Reads of the trigger needed to latch a fresh sample.  One returns the
+ * previous latch, which the documented procedures work around by reading it
+ * three times.
+ */
+#define SIT9531X_DBG_LATCH_READS		3
+#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
+#define SIT9531X_TDC_MAG_HI_MASK	GENMASK(2, 0)
+
 /*
  * DIVN carried as fixed point, and the unit the DPLL ABI wants the
  * fractional frequency offset in.  Equal in value, distinct in meaning.
-- 
2.39.2 (Apple Git-143)


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

* [PATCH net-next v9 11/15] dpll: sit9531x: add support to get and set esync on pins
  2026-09-15  0:00 [PATCH net-next v9 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (10 preceding siblings ...)
  2026-09-15  0:00 ` [PATCH net-next v9 12/15] dpll: sit9531x: add support to get phase offset on the connected input pin Ali Rouhi
@ 2026-09-15  0:00 ` Ali Rouhi
  2026-09-17 15:01   ` netdev-bot+sashiko
  2026-09-15  0:00 ` [PATCH net-next v9 13/15] dpll: sit9531x: add support to get fractional frequency offset Ali Rouhi
                   ` (2 subsequent siblings)
  14 siblings, 1 reply; 33+ messages in thread
From: Ali Rouhi @ 2026-09-15  0:00 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel, Ali Rouhi

From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>

Embedded sync marks a one-pulse-per-second boundary inside a higher-rate
output by widening one pulse.  On this device that is the same divider
programming as a frequency change plus the per-output pulse control, so
enabling it sets the output to 1 Hz and disabling it restores the
requested rate.

Only outputs the firmware describes as esync-controllable offer it; on the
rest the operations are refused rather than silently reprogramming an
output whose board wiring does not expect it.  The pulse width is fixed at
half the period, which is what the device produces, and the core is told
the supported range rather than left to guess.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/core.c | 141 ++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/core.h |   2 +
 drivers/dpll/sit9531x/dpll.c | 154 ++++++++++++++++++++++++++++++++++-
 drivers/dpll/sit9531x/dpll.h |   3 +-
 drivers/dpll/sit9531x/regs.h |   7 ++
 5 files changed, 305 insertions(+), 2 deletions(-)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 8d857f1a0c89..46179a9e13c3 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -1654,6 +1654,103 @@ int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
 	return rc;
 }
 
+/*
+ * sit9531x_output_pulse_write - write an output's PROG0 pulse control
+ *
+ * The caller must already be in the programming state.
+ */
+static int sit9531x_output_pulse_write(struct sit9531x_dev *sitdev, u8 out_idx,
+				       u8 pulse_ctrl)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	u8 slot, page, reg;
+
+	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);
+
+	return sit9531x_write_u8(sitdev, SIT9531X_REG(page, reg), pulse_ctrl);
+}
+
+/*
+ * sit9531x_output_esync_program - set carrier, marker and enable at once
+ *
+ * Turning embedded sync on means three things to the output system: the
+ * carrier rate, the pulse generator that puts the marker on it, and the
+ * output enable.  The device takes them all inside one programming state
+ * -- our validated divider sequence writes a whole register group
+ * that way -- so doing them as three sequences would pay the settling
+ * time three times, with the subsystem's device lock held throughout.
+ *
+ * The phase flush stays after the latch: it aligns the output to the
+ * divider the device is running, not to the one it was asked for.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_output_esync_program(struct sit9531x_dev *sitdev, u8 out_idx,
+				  u8 pll_idx, u64 carrier, u8 pulse_ctrl)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	u64 fvco, divo;
+	bool muted;
+	int rc, ret;
+	u8 slot;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	rc = sit9531x_output_divo_calc(sitdev, out_idx, pll_idx, carrier,
+				       &fvco, &divo);
+	if (rc)
+		return rc;
+
+	slot = info->clkout_map[out_idx];
+
+	rc = sit9531x_prg_enter(sitdev);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_output_divo_write(sitdev, out_idx, divo);
+	if (!rc)
+		rc = sit9531x_output_pulse_write(sitdev, out_idx, pulse_ctrl);
+	/*
+	 * Keep the mute the user asked for.  Embedded sync changes what the
+	 * output carries, not whether it is driven, so an output muted
+	 * through pin-state stays muted.
+	 */
+	if (!rc)
+		rc = sit9531x_output_hiz_write(sitdev, slot,
+					       !sitdev->out[out_idx].enabled);
+
+	ret = sit9531x_prg_commit(sitdev);
+	if (ret && !rc)
+		rc = ret;
+
+	if (!sit9531x_output_forced_hiz(sitdev, out_idx, &muted))
+		sitdev->out[out_idx].enabled = !muted;
+
+	if (rc)
+		return rc;
+
+	rc = sit9531x_output_phase_flush(sitdev, pll_idx);
+	if (rc)
+		return rc;
+
+	sitdev->out[out_idx].freq = div64_u64(fvco, divo);
+
+	/*
+	 * The delay registers count VCO cycles against the output period in
+	 * force when they were written, so the carrier change re-times a
+	 * phase adjust the same way a frequency set does.
+	 */
+	if (sitdev->out[out_idx].phase_adj)
+		return sit9531x_output_phase_adjust_set(sitdev, out_idx,
+							sitdev->out[out_idx].phase_adj);
+
+	return 0;
+}
+
 /*
  * sit9531x_output_freq_get - read output clock frequency from hardware
  * @out_idx:	output index (0-N for this chip variant)
@@ -1991,6 +2088,50 @@ int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
 	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;
+	int rc, ret;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (out_idx >= info->num_outputs)
+		return -EINVAL;
+
+	/*
+	 * 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_output_pulse_write(sitdev, out_idx, 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;
+}
+
 /*
  * sit9531x_ref_state_fetch - read input reference status from hardware
  * @index:	logical input index
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 06df7a91a7c6..1f77ff28ec81 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -281,6 +281,8 @@ 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_esync_program(struct sit9531x_dev *sitdev, u8 out_idx,
+				  u8 pll_idx, u64 carrier, u8 pulse_ctrl);
 int sit9531x_output_pulse_ctrl_set(struct sit9531x_dev *sitdev,
 				   u8 out_idx, u8 pulse_ctrl);
 
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index 78034b7f089c..0cad081eb599 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -22,9 +22,21 @@
 #include "prop.h"
 #include "regs.h"
 
-#define SIT9531X_ESYNC_FREQ_10MHZ	10000000ULL
+#define SIT9531X_ESYNC_FREQ_1HZ	DPLL_PIN_FREQUENCY_1_HZ
+#define SIT9531X_ESYNC_FREQ_10MHZ	DPLL_PIN_FREQUENCY_10_MHZ
 #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_1HZ),
+};
+
+static bool
+sit9531x_dpll_esync_pin_supported(const struct sit9531x_dpll_pin *dpin)
+{
+	return dpin->esync_control;
+}
+
 static bool sit9531x_dpll_is_input_pin(const struct sit9531x_dpll_pin *pin)
 {
 	return pin->dir == DPLL_PIN_DIRECTION_INPUT;
@@ -726,6 +738,12 @@ sit9531x_dpll_output_pin_frequency_set(const struct dpll_pin *pin,
 	actual_pll = sitdev->out[dpin->id].pll_idx;
 
 	mutex_lock(&sitdev->multiop_lock);
+	if (dpin->esync_freq) {
+		mutex_unlock(&sitdev->multiop_lock);
+		NL_SET_ERR_MSG(extack,
+			       "Disable embedded sync on this pin before changing frequency");
+		return -EBUSY;
+	}
 	rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll,
 				      frequency);
 	mutex_unlock(&sitdev->multiop_lock);
@@ -869,6 +887,138 @@ sit9531x_dpll_output_pin_phase_adjust_set(const struct dpll_pin *pin,
 	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;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+
+	if (!sit9531x_dpll_esync_pin_supported(dpin))
+		return -EOPNOTSUPP;
+
+	mutex_lock(&sitdev->multiop_lock);
+	esync->range = sit9531x_esync_ranges;
+	esync->range_num = ARRAY_SIZE(sit9531x_esync_ranges);
+	esync->freq = dpin->esync_freq;
+	/*
+	 * The hardware PROG0 byte is programmed with the same literal that the
+	 * ABI reports as pulse percent for the supported 1 Hz mode. When esync
+	 * is disabled, report pulse as zero to avoid implying an active marker.
+	 */
+	esync->pulse = dpin->esync_freq ? SIT9531X_ESYNC_PULSE_DEFAULT : 0;
+	mutex_unlock(&sitdev->multiop_lock);
+
+	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);
+
+	if (!freq) {
+		u64 prev_freq = dpin->esync_prev_freq;
+		u8 pulse = SIT9531X_ESYNC_PULSE_DEFAULT;
+
+		if (!dpin->esync_freq) {
+			mutex_unlock(&sitdev->multiop_lock);
+			return 0;
+		}
+		rc = sit9531x_output_pulse_ctrl_set(sitdev, dpin->id, 0);
+		if (!rc && prev_freq)
+			rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll, prev_freq);
+		if (rc)
+			sit9531x_output_pulse_ctrl_set(sitdev, dpin->id, pulse);
+		if (!rc)
+			dpin->esync_freq = 0;
+		mutex_unlock(&sitdev->multiop_lock);
+		if (rc)
+			NL_SET_ERR_MSG(extack,
+				       "Failed to turn embedded sync off on this output");
+		return rc;
+	}
+
+	if (freq != SIT9531X_ESYNC_FREQ_1HZ) {
+		mutex_unlock(&sitdev->multiop_lock);
+		NL_SET_ERR_MSG(extack,
+			       "Only 1 Hz embedded-sync frequency is supported");
+		return -EINVAL;
+	}
+
+	if (dpin->esync_freq == SIT9531X_ESYNC_FREQ_1HZ) {
+		mutex_unlock(&sitdev->multiop_lock);
+		return 0;
+	}
+
+	if (!dpin->esync_freq) {
+		/*
+		 * Remember the carrier to restore.  The cached rate is zero
+		 * for an output whose firmware node lists no frequency and
+		 * which has never been set, so read the divider rather than
+		 * leave the output parked on the esync carrier at disable.
+		 */
+		dpin->esync_prev_freq = sitdev->out[dpin->id].freq;
+		if (!dpin->esync_prev_freq)
+			sit9531x_output_freq_get(sitdev, dpin->id,
+						 &dpin->esync_prev_freq);
+	}
+
+	/*
+	 * Carrier, marker and enable go into the device in one programming
+	 * sequence.  Without the pulse generator (PROG0 PULSE_CTRL) the
+	 * output would carry 10 MHz and no marker at all;
+	 * SIT9531X_ESYNC_PULSE_DEFAULT is the duty esync_get advertises for
+	 * the 1 Hz embedded-sync signal.
+	 */
+	rc = sit9531x_output_esync_program(sitdev, dpin->id, actual_pll,
+					   SIT9531X_ESYNC_FREQ_10MHZ,
+					   SIT9531X_ESYNC_PULSE_DEFAULT);
+	if (rc) {
+		sit9531x_output_pulse_ctrl_set(sitdev, dpin->id, 0);
+		if (dpin->esync_prev_freq)
+			sit9531x_output_freq_set(sitdev, dpin->id,
+						 actual_pll,
+						 dpin->esync_prev_freq);
+	}
+
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc) {
+		NL_SET_ERR_MSG(extack,
+			       "Failed to program embedded sync on this output");
+		return rc;
+	}
+
+	dpin->esync_freq = SIT9531X_ESYNC_FREQ_1HZ;
+
+	return 0;
+}
+
 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,
@@ -877,6 +1027,8 @@ static const struct dpll_pin_ops sit9531x_dpll_output_pin_ops = {
 	.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 *
diff --git a/drivers/dpll/sit9531x/dpll.h b/drivers/dpll/sit9531x/dpll.h
index 901aec238f4f..0b8b24cadcbb 100644
--- a/drivers/dpll/sit9531x/dpll.h
+++ b/drivers/dpll/sit9531x/dpll.h
@@ -34,7 +34,8 @@ struct sit9531x_dpll_pin {
 	/* in 1/DPLL_PHASE_OFFSET_DIVIDER picosecond units */
 	s64				phase_offset;
 	bool				esync_control;
-	u64				esync_freq;	/* 0 == disabled */
+	u64				esync_freq;	/* 0 == disabled, 1 Hz == enabled */
+	u64				esync_prev_freq;	/* carrier to restore on disable */
 };
 
 /* Per-PLL DPLL device state. */
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index f40762f06403..9d515e109feb 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -230,6 +230,13 @@
 #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
-- 
2.39.2 (Apple Git-143)


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

* [PATCH net-next v9 13/15] dpll: sit9531x: add support to get fractional frequency offset
  2026-09-15  0:00 [PATCH net-next v9 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (11 preceding siblings ...)
  2026-09-15  0:00 ` [PATCH net-next v9 11/15] dpll: sit9531x: add support to get and set esync on pins Ali Rouhi
@ 2026-09-15  0:00 ` Ali Rouhi
  2026-09-17 15:01   ` netdev-bot+sashiko
  2026-09-15  0:00 ` [PATCH net-next v9 14/15] dpll: sit9531x: model the inter-PLL sync net as a pair of pins Ali Rouhi
  2026-09-15  0:00 ` [PATCH net-next v9 15/15] dpll: sit9531x: allow the device tree to override two board facts Ali Rouhi
  14 siblings, 1 reply; 33+ messages in thread
From: Ali Rouhi @ 2026-09-15  0:00 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel, Ali Rouhi

From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>

Report how far a PLL's reference is from nominal, as a fraction of the
nominal rate.

The device does not measure that directly, but it does run a feedback
divider that the loop adjusts to keep the reference in step: the
difference between the divider the loop is running and the divider the
configuration asked for is exactly the offset of the reference.  Both are
read from the chip -- the configured one from its registers, the running
one through the debug window -- and the offset falls out of the ratio.

The running value comes from the same latched debug window as the phase
offset, so it is triggered three times per sample for the same reason.

The running divider is two numbers read through a window that exposes one
tap at a time, so the numerator and the denominator come from separate
samples and a loop moving between them can tear the pair.  On a locked
loop the divider moves by parts per trillion between two register reads,
which is below what this measurement resolves; the part offers no way to
latch both at once.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/core.c | 194 +++++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/dpll.c |  51 +++++++++
 drivers/dpll/sit9531x/regs.h |  12 +++
 3 files changed, 257 insertions(+)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 356d1eac4ec5..88bfa63b2975 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -1224,6 +1224,76 @@ static int sit9531x_is_xo_doubler_enabled(struct sit9531x_dev *sitdev)
 	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, bool low_freq_clk,
+			       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;
+
+	/*
+	 * Select the debug clock this tap needs.  The device never clears
+	 * the bit, so a read that left it to whatever the previous one set
+	 * would depend on the order the taps happened to be read in.  Taps
+	 * below 200 kHz need the slow clock; the divider taps do not.
+	 */
+	rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+				    SIT9531X_PLL_REG_DBG_WRITE_CODE,
+				    SIT9531X_DBG_LOW_FREQ_CLK_BIT,
+				    low_freq_clk ?
+				    SIT9531X_DBG_LOW_FREQ_CLK_BIT : 0);
+	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 sample of the selected tap.  Read it
+	 * three times, as the documented phase-difference procedure does and as
+	 * sit9531x_phase_offset_read() already did: a single read returns
+	 * the previous latch, so a caller sampling repeatedly gets the same
+	 * value back however much the tap has moved.
+	 */
+	for (i = 0; i < SIT9531X_DBG_LATCH_READS; i++) {
+		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
@@ -1291,6 +1361,130 @@ static int sit9531x_divn_static(struct sit9531x_dev *sitdev, u8 pll_idx,
 	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.
+ *
+ * The denominator is taken as it reads.  The configuration register holds
+ * the divisor minus one, and correcting for that reproduces a profile's
+ * stated VCO exactly, to the last bit of the fraction; this tap is a wider
+ * field sampled from the loop itself and the documented readback uses it as it
+ * comes, so the bias is not applied here.
+ *
+ * The numerator and the denominator come from two separate latches:
+ * the debug window exposes one tap at a time, so the pair can be torn
+ * by a loop that moves between them.  The divider changes by parts per
+ * trillion between samples on a locked loop, which is below what this
+ * measurement resolves.
+ *
+ * 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,
+				 false, 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, false, 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;
+
+	/*
+	 * sit9531x_divn_fixed() drops the fractional part when the
+	 * denominator is zero.  That is right for a configuration register
+	 * that was never programmed, but a running loop reading zero means
+	 * the sample is unusable, and returning the whole part alone would
+	 * put a wrong DIVN into the frequency offset without saying so.
+	 */
+	if (!fracd)
+		return -ENODATA;
+
+	*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;
+	/*
+	 * Scaling by a divider far below one whole cycle overflows the
+	 * 64-bit product and comes back as ~0, which would be reported
+	 * as a plausible offset.  A DIVN that small is not a running
+	 * loop, so treat it as no measurement.
+	 */
+	if (configured < SIT9531X_DIVN_SCALE)
+		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
  *
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index 1dfd83b635e2..2a973364a9cd 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -603,6 +603,50 @@ sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
 	return 0;
 }
 
+/*
+ * 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;
+	enum dpll_pin_state state;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	/*
+	 * Publish FFO only for the input the DPLL is actively tracking.
+	 * selected_ref alone is not enough (free-run, LOL, holdover), so use
+	 * the same CONNECTED criterion as the generic selection-state logic.
+	 */
+	sit9531x_dpll_selection_state_get(sitdev, sitdpll, dpin->id, &state);
+	if (state != DPLL_PIN_STATE_CONNECTED) {
+		mutex_unlock(&sitdev->multiop_lock);
+		return -ENODATA;
+	}
+
+	rc = sit9531x_pll_ffo_ppt(sitdev, sitdpll->id, &ffo->ffo);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc && rc != -ENODATA)
+		NL_SET_ERR_MSG(extack,
+			       "Failed to measure the frequency offset of the selected reference");
+
+	return rc;
+}
+
 /*
  * sit9531x_dpll_input_pin_phase_offset_get - phase offset of a reference
  *
@@ -712,6 +756,13 @@ static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
 	.prio_get		= sit9531x_dpll_input_pin_prio_get,
 	.prio_set		= sit9531x_dpll_input_pin_prio_set,
 	.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,
 };
 
 /*
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index 08eae8a2c279..78e8a47e96ab 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -309,6 +309,18 @@
 #define SIT9531X_TDC_SIGN_BIT		3
 #define SIT9531X_TDC_MAG_HI_MASK	GENMASK(2, 0)
 
+/*
+ * 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.
-- 
2.39.2 (Apple Git-143)


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

* [PATCH net-next v9 14/15] dpll: sit9531x: model the inter-PLL sync net as a pair of pins
  2026-09-15  0:00 [PATCH net-next v9 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (12 preceding siblings ...)
  2026-09-15  0:00 ` [PATCH net-next v9 13/15] dpll: sit9531x: add support to get fractional frequency offset Ali Rouhi
@ 2026-09-15  0:00 ` Ali Rouhi
  2026-09-17 15:01   ` netdev-bot+sashiko
  2026-09-15  0:00 ` [PATCH net-next v9 15/15] dpll: sit9531x: allow the device tree to override two board facts Ali Rouhi
  14 siblings, 1 reply; 33+ messages in thread
From: Ali Rouhi @ 2026-09-15  0:00 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel, Ali Rouhi

From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>

The device has an internal net by which one PLL can drive the others: the
source PLL puts its output on it, and any other PLL can select it as a
reference instead of an external input.  The two ends are nothing alike --
one is driven, the other is selected -- so they are two pins rather than
one: an output pin on the source and an input pin on each destination.

That keeps each pin honest about what its state means.  The source pin
reports whether this PLL is the one driving the net, and setting it takes
the net over or gives it up; a destination pin reports whether its PLL has
selected the net, and behaves like any other selectable input.  A single
pin would have had to answer both questions at once and could only have
been right about one of them.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/core.c | 243 ++++++++++++++++++++++++++++++++++-
 drivers/dpll/sit9531x/core.h |   3 +
 drivers/dpll/sit9531x/dpll.c | 235 ++++++++++++++++++++++++++++++++-
 drivers/dpll/sit9531x/regs.h |   3 +
 4 files changed, 476 insertions(+), 8 deletions(-)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 88bfa63b2975..b15859890caa 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -2024,12 +2024,13 @@ int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
  *   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).
+ * Slots 0-5 live on Page 3, slots 6-11 on Page 4, with each slot's
+ * block at base = 0x15 + 16 * (slot % 6); the slot is the physical
+ * output position from clkout_map[], not the logical output index.
  *
- * 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.
+ * The chip only supports unsigned positive delay.  Requests are folded
+ * modulo one output period: positive delays wrap naturally and a negative
+ * phase adjustment (advance) is rendered as (T_out - |phase|).
  */
 
 int sit9531x_output_phase_adjust_set(struct sit9531x_dev *sitdev,
@@ -2282,6 +2283,225 @@ int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
 	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 },
+};
+
+int sit9531x_intsync_src_detect(struct sit9531x_dev *sitdev)
+{
+	s8 src = -1;
+	u8 global;
+	u8 pll, ext_page;
+	int rc, i;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &global);
+	if (rc)
+		return rc;
+
+	if (!(global & BIT(SIT9531X_INTSYNC_EN_BIT))) {
+		sitdev->intsync_src = -1;
+		return 0;
+	}
+
+	for (pll = 0; pll < SIT9531X_NUM_PLLS; pll++) {
+		ext_page = SIT9531X_PLL_EXT_PAGE(pll);
+
+		for (i = 0; i < ARRAY_SIZE(intsync_config); i++) {
+			u16 reg;
+			u8 val;
+
+			reg = SIT9531X_REG(ext_page, intsync_config[i].offset);
+
+			rc = sit9531x_read_u8(sitdev, reg, &val);
+			if (rc)
+				return rc;
+			if (val != intsync_config[i].en_val)
+				break;
+		}
+
+		if (i == ARRAY_SIZE(intsync_config)) {
+			/*
+			 * Only one PLL can drive the net.  If a second
+			 * one matches, the registers are not describing
+			 * a state this driver put the device in, so say
+			 * so rather than pick silently.
+			 */
+			if (src < 0)
+				src = pll;
+			else
+				dev_warn(sitdev->dev,
+					 "PLL%c also matches the INTSYNC source pattern; keeping PLL%c\n",
+					 'A' + pll, 'A' + src);
+		}
+	}
+
+	sitdev->intsync_src = src;
+
+	return 0;
+}
+
+/*
+ * 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);
+	usleep_range(1000, 2000);
+	if (rc)
+		goto err_disable;
+
+	/* Unlock debug on EXT page */
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(ext_page, SIT9531X_PLL_REG_DEBUG),
+			       SIT9531X_PLL_DEBUG_UNLOCK);
+	if (rc)
+		goto err_disable;
+
+	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)
+			goto err_disable;
+	}
+
+	/* 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)
+		goto err_disable;
+
+	return 0;
+
+err_disable:
+	/*
+	 * The global enable is already set at this point.  The caller only
+	 * records the source PLL when this function succeeds, so nothing
+	 * else will ever clear the bit: undo it here rather than leave the
+	 * net asserted with a half-written EXT page.
+	 */
+	{
+		int rollback_rc;
+
+		rollback_rc = sit9531x_intsync_disable(sitdev, src_pll_idx);
+		if (rollback_rc)
+			dev_warn(sitdev->dev,
+				 "INTSYNC rollback failed after enable error: %d (original %d)\n",
+				 rollback_rc, rc);
+	}
+
+	return rc;
+}
+
+/*
+ * 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);
+	usleep_range(1000, 2000);
+	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)
@@ -2875,6 +3095,15 @@ static int sit9531x_dev_state_fetch(struct sit9531x_dev *sitdev)
 		return rc;
 	}
 
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_intsync_src_detect(sitdev);
+	mutex_unlock(&sitdev->multiop_lock);
+	if (rc) {
+		dev_err(sitdev->dev,
+			"Failed to detect INTSYNC source: %d\n", rc);
+		return rc;
+	}
+
 	for (i = 0; i < sitdev->info->num_outputs; i++) {
 		rc = sit9531x_out_state_fetch(sitdev, i);
 		if (rc) {
@@ -3355,13 +3584,13 @@ static bool sit9531x_dpll_pin_is_registrable(struct sit9531x_dpll *sitdpll,
 		if (index == SIT9531X_MAX_INPUTS)
 			return true;
 		if (index == SIT9531X_INTSYNC_PIN_ID)
-			return false;
+			return true;
 
 		return sit9531x_input_pin_is_registrable(sitdev, index);
 	}
 
 	if (index == SIT9531X_INTSYNC_OUT_PIN_ID)
-		return false;
+		return true;
 
 	if (index >= sitdev->info->num_outputs)
 		return false;
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 1f77ff28ec81..5275733a3b4a 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -252,6 +252,8 @@ 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);
+bool sit9531x_input_prio_present(struct sit9531x_dev *sitdev,
+				 u8 pll_idx, u8 input_idx);
 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,
@@ -279,6 +281,7 @@ 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);
+int sit9531x_intsync_src_detect(struct sit9531x_dev *sitdev);
 
 /* ---- Output pulse control ---- */
 int sit9531x_output_esync_program(struct sit9531x_dev *sitdev, u8 out_idx,
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index 2a973364a9cd..3efe6fc90701 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -42,6 +42,20 @@ static bool sit9531x_dpll_is_input_pin(const struct sit9531x_dpll_pin *pin)
 	return pin->dir == DPLL_PIN_DIRECTION_INPUT;
 }
 
+static 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 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 bool
 sit9531x_dpll_is_xo_pin(const struct sit9531x_dpll_pin *pin)
 {
@@ -792,8 +806,222 @@ sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
 				       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, detect_rc = 0;
+
+	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.
+		 */
+		if (sit9531x_input_prio_present(sitdev, sitdpll->id,
+						sit9531x_input_hw_src(SIT9531X_INTSYNC_PIN_ID))) {
+			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);
+		break;
+	case DPLL_PIN_STATE_DISCONNECTED:
+		if (sitdev->intsync_src != sitdpll->id)
+			break;
+		rc = sit9531x_intsync_disable(sitdev, sitdpll->id);
+		break;
+	default:
+		rc = -EINVAL;
+		break;
+	}
+
+	/*
+	 * Re-scan hardware after source state transitions so cache follows
+	 * partially failed enable/disable paths as closely as possible.
+	 */
+	if (state == DPLL_PIN_STATE_CONNECTED ||
+	    state == DPLL_PIN_STATE_DISCONNECTED)
+		detect_rc = sit9531x_intsync_src_detect(sitdev);
+	/*
+	 * The refresh only re-reads what the device now shows.  Failing
+	 * the request because that read hit a bus error would tell
+	 * userspace the enable did not happen when it did.
+	 */
+	if (detect_rc)
+		dev_warn(sitdev->dev,
+			 "INTSYNC source cache not refreshed: %d\n",
+			 detect_rc);
+
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc && rc != -EBUSY && rc != -EINVAL && rc != -EOPNOTSUPP)
+		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
+ *
+ * Selection role, so the contract above decides this exactly as it does
+ * for a physical input: the priority table is the eligibility record, and
+ * whether a source PLL happens to be driving the net right now is no more
+ * a state than a momentary LOS is on an external reference.  The one
+ * addition is that the PLL driving INTSYNC is never 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;
+
+	mutex_lock(&sitdev->multiop_lock);
+	if (sitdev->intsync_src == sitdpll->id)
+		*state = DPLL_PIN_STATE_DISCONNECTED;
+	else
+		sit9531x_dpll_selection_state_get(sitdev, sitdpll,
+						  SIT9531X_INTSYNC_PIN_ID,
+						  state);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_intsync_dst_state_on_dpll_set - lock a PLL to INTSYNC
+ *
+ * Selection role, so this accepts and refuses what a physical input does,
+ * CONNECTED included: the device pins no reference on request whichever
+ * source is asked for.  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_CONNECTED:
+		NL_SET_ERR_MSG(extack,
+			       "Device selects its reference by priority; use selectable");
+		rc = -EOPNOTSUPP;
+		break;
+	case DPLL_PIN_STATE_SELECTABLE:
+		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 == -EBUSY)
+		NL_SET_ERR_MSG(extack,
+			       "Only source left in the priority table; it cannot be emptied");
+	else if (rc && rc != -EINVAL && rc != -EOPNOTSUPP)
+		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
  *
@@ -1187,8 +1415,13 @@ static const struct dpll_pin_ops sit9531x_dpll_output_pin_ops = {
 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_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;
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index 78e8a47e96ab..3e902a169937 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -334,6 +334,9 @@
 #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)
-- 
2.39.2 (Apple Git-143)


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

* [PATCH net-next v9 15/15] dpll: sit9531x: allow the device tree to override two board facts
  2026-09-15  0:00 [PATCH net-next v9 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (13 preceding siblings ...)
  2026-09-15  0:00 ` [PATCH net-next v9 14/15] dpll: sit9531x: model the inter-PLL sync net as a pair of pins Ali Rouhi
@ 2026-09-15  0:00 ` Ali Rouhi
  2026-09-17 15:02   ` netdev-bot+sashiko
  14 siblings, 1 reply; 33+ messages in thread
From: Ali Rouhi @ 2026-09-15  0:00 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel, Ali Rouhi

From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>

Two things the driver reads from the chip can be wrong on a board, and
neither has anywhere else to come from.

The VCO frequency is derived from the feedback divider, which is exact
while the loop runs but not while a PLL sits in free-run with a divider
the configuration never programmed; a board that knows its own VCO can
state it.  The output-to-PLL routing is read from the output map
registers, which describe what the loaded configuration did -- and a board
whose outputs are fanned out differently from what those registers imply
can state that too.

Both are optional.  Absent the properties the driver behaves exactly as
before, deriving one and reading the other.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/core.c | 104 +++++++++++++++++++++++++++++++++++
 1 file changed, 104 insertions(+)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index b15859890caa..51f7d8a4ea44 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -3862,6 +3862,108 @@ static u64 sit9531x_derive_clock_id(struct sit9531x_dev *sitdev)
 	return clkid;
 }
 
+/*
+ * Does an Fvco fall in the band the given PLL runs in?
+ *
+ * The two bands are disjoint, and which one applies is fixed per PLL, so a
+ * single envelope from the bottom of the low band to the top of the high
+ * one would accept both the ~1 GHz gap between them and a rate belonging to
+ * the other PLL's band.
+ */
+static bool sit9531x_fvco_in_band(u8 pll_idx, u64 fvco)
+{
+	if (pll_idx == 1 || pll_idx == 3)
+		return fvco >= SIT9531X_FVCO_HIGHBAND_MIN &&
+		       fvco <= SIT9531X_FVCO_HIGHBAND_MAX;
+
+	return fvco >= SIT9531X_FVCO_LOWBAND_MIN &&
+	       fvco <= SIT9531X_FVCO_LOWBAND_MAX;
+}
+
+/*
+ * 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, rc;
+
+	if (device_property_present(sitdev->dev, "sitime,pll-fvco")) {
+		rc = device_property_read_u64_array(sitdev->dev,
+						    "sitime,pll-fvco",
+						    sitdev->pll_fvco,
+						    SIT9531X_NUM_PLLS);
+		if (rc) {
+			dev_warn(sitdev->dev,
+				 "invalid sitime,pll-fvco (%d), ignoring\n",
+				 rc);
+			memset(sitdev->pll_fvco, 0, sizeof(sitdev->pll_fvco));
+		}
+
+		/*
+		 * The override is used verbatim by the divider math, so an
+		 * implausible value (units typo, wrong cell count worked
+		 * around with zeros) must not silently misprogram DIVO.
+		 * Anything outside both VCO bands is dropped with a warning
+		 * rather than trusted.
+		 */
+		for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+			u64 f = sitdev->pll_fvco[i];
+
+			if (f && !sit9531x_fvco_in_band(i, f)) {
+				dev_warn(sitdev->dev,
+					 "PLL%c Fvco override %llu Hz is outside the band that PLL runs in, ignoring\n",
+					 'A' + i, f);
+				sitdev->pll_fvco[i] = 0;
+			}
+		}
+	}
+
+	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)) {
+		dev_warn(sitdev->dev,
+			 "invalid sitime,output-pll-map, ignoring\n");
+		return;
+	}
+
+	/*
+	 * The binding allows only 0-3 and 255 per entry.  A stray value
+	 * would silently unroute an output (m >= SIT9531X_NUM_PLLS reads
+	 * as unmapped in sit9531x_out_state_fetch()), so reject the whole
+	 * property loudly instead.
+	 */
+	for (i = 0; i < n; i++) {
+		if (map[i] >= SIT9531X_NUM_PLLS &&
+		    map[i] != SIT9531X_OUT_PLL_UNMAPPED) {
+			dev_warn(sitdev->dev,
+				 "sitime,output-pll-map entry %d is %u (must be 0-3 or 255), ignoring map\n",
+				 i, map[i]);
+			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;
@@ -3906,6 +4008,8 @@ int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
 	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;
-- 
2.39.2 (Apple Git-143)


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

* Re: [PATCH net-next v9 03/15] dpll: add basic SiTime SiT9531x support
  2026-09-15  0:00 ` [PATCH net-next v9 03/15] dpll: add basic SiTime SiT9531x support Ali Rouhi
@ 2026-09-17  8:42   ` Ivan Vecera
  2026-09-17 15:01   ` netdev-bot+sashiko
  1 sibling, 0 replies; 33+ messages in thread
From: Ivan Vecera @ 2026-09-17  8:42 UTC (permalink / raw)
  To: Ali Rouhi, jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, robh, krzk+dt, conor+dt,
	cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel

On 9/15/26 2:00 AM, Ali Rouhi wrote:
> From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
> 
> The SiT9531x is an I2C clock generator with four independent PLLs, up to
> eight input clocks and up to twelve outputs.  Later patches register the
> PLLs with the DPLL subsystem and expose the clocks as pins; this one adds
> only what is needed to reach the device.
> 
> The register space is paged: 32 pages of 256 registers, selected by
> writing the page number to offset 0xFF, which is present in every page.
> A regmap range configuration describes that window, so the rest of the
> driver addresses a register as a page and an offset and never touches the
> selector itself.  Pages come in pairs for the PLLs (0x0A/0x1A for PLLA,
> and so on).
> 
> Probe reads the rate of the crystal feeding XIN, since every frequency
> the driver later computes derives from it; takes the optional reset line
> and leaves it deasserted, because the device configuration comes from
> efuse or from a blob applied before probe and a reset would discard it;
> then identifies the variant from the single byte at page 0 offset 0x02
> and refuses to bind on anything unknown.
> 
> 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                    |   1 +
>   drivers/dpll/Kconfig           |   2 +
>   drivers/dpll/Makefile          |   1 +
>   drivers/dpll/sit9531x/Kconfig  |  17 +++
>   drivers/dpll/sit9531x/Makefile |   4 +
>   drivers/dpll/sit9531x/core.c   | 272 +++++++++++++++++++++++++++++++++
>   drivers/dpll/sit9531x/core.h   |  91 +++++++++++
>   drivers/dpll/sit9531x/regs.h   |  56 +++++++
>   8 files changed, 444 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/regs.h
> 
> ...
> +
> +#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,
> +};

Couldn't it be useful to cache at least page selector register or is it
volatile? I remember from zl3073x history that page selector caching
helped a lot to reduce number of i2c transactions.


Thanks,
Ivan


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

* Re: [PATCH net-next v9 04/15] dpll: sit9531x: read DPLL types and pin properties from system firmware
  2026-09-15  0:00 ` [PATCH net-next v9 04/15] dpll: sit9531x: read DPLL types and pin properties from system firmware Ali Rouhi
@ 2026-09-17  9:42   ` Ivan Vecera
  2026-09-17 15:01   ` netdev-bot+sashiko
  1 sibling, 0 replies; 33+ messages in thread
From: Ivan Vecera @ 2026-09-17  9:42 UTC (permalink / raw)
  To: Ali Rouhi, jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, robh, krzk+dt, conor+dt,
	cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel



On 9/15/26 2:00 AM, Ali Rouhi wrote:
> From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
> 
> The DPLL core wants a type per device and a property set per pin: a
> label, the direction, the capabilities, and the frequencies the pin
> supports.  None of that can be read from the chip -- which frequencies a
> board actually presents on a given input, and whether an output's
> embedded sync is wired to be controllable, are board facts -- so they
> come from the firmware node, with defaults for a node that does not
> describe them.
> 
> Input pins are interleaved P and N lanes of four differential pairs, so a
> logical index maps to a pair and a lane, and a pair configured
> single-ended presents two independent inputs where a differential one
> presents one.  The labels follow from that, and the two extra input
> positions -- the crystal and the inter-PLL sync net -- are named
> separately.
> 
> Kept in its own file, and introduced before anything is registered, so
> the registration code that follows has nothing to say about firmware.
> 
> Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
> Assisted-by: Claude:claude-4-opus [chat]
> Signed-off-by: Ali Rouhi <arouhi@sitime.com>
> ---
>   drivers/dpll/sit9531x/Makefile |   2 +-
>   drivers/dpll/sit9531x/core.h   |  72 ++++++
>   drivers/dpll/sit9531x/prop.c   | 437 +++++++++++++++++++++++++++++++++
>   drivers/dpll/sit9531x/prop.h   |  39 +++
>   4 files changed, 549 insertions(+), 1 deletion(-)
>   create mode 100644 drivers/dpll/sit9531x/prop.c
>   create mode 100644 drivers/dpll/sit9531x/prop.h
> 
> ...
> +
> +               /*
> +                * Allow phase-adjust over a +/-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.  Only outputs get a range: input pins
> +                * have no .phase_adjust_set, and advertising one there would
> +                * promise userspace something every set would refuse.
> +                */
> +               props->dpll_props.phase_range.min = -1000000000; /* -1 ms in ps */
> +               props->dpll_props.phase_range.max =  1000000000; /* +1 ms in ps */
> +               /*
> +                * The fine step is 30 ps, but requests are accepted at 1 ps
> +                * resolution and rounded to the nearest achievable delay, so
> +                * advertise the request granularity, not the hardware step.
> +                */
> +               props->dpll_props.phase_gran = 1;

What is the real HW granularity? If 30ps then you should announce 30 to
inform the userspace that this is the step supported by the HW.

Thanks,
Ivan


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

* Re: [PATCH net-next v9 10/15] dpll: sit9531x: add support to adjust output phase
  2026-09-15  0:00 ` [PATCH net-next v9 10/15] dpll: sit9531x: add support to adjust output phase Ali Rouhi
@ 2026-09-17  9:55   ` Ivan Vecera
  2026-09-17 15:01   ` netdev-bot+sashiko
  1 sibling, 0 replies; 33+ messages in thread
From: Ivan Vecera @ 2026-09-17  9:55 UTC (permalink / raw)
  To: Ali Rouhi, jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, robh, krzk+dt, conor+dt,
	cjubran, pabeni, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel



On 9/15/26 2:00 AM, Ali Rouhi wrote:
> From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
> 
> Shift an output in time against the others driven by the same PLL.  The
> device has a coarse delay counted in VCO cycles and a three-bit fine field
> in fixed thirty-picosecond steps, so a requested offset is split between
> the two and what the core reads back is what the registers hold rather
> than what was asked for.
> 
> Delay only ever advances, so an offset larger than one output period is
> folded back into a single period -- for a periodic signal that is the same
> phase.  The write takes effect in the programming state, which is left
> with the loops re-locked even when a write inside it failed.
> 
> Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
> Assisted-by: Claude:claude-4-opus [chat]
> Signed-off-by: Ali Rouhi <arouhi@sitime.com>
> ---
>   drivers/dpll/sit9531x/core.c | 212 ++++++++++++++++++++++++++++++++++-
>   drivers/dpll/sit9531x/core.h |   4 +
>   drivers/dpll/sit9531x/dpll.c |  56 +++++++++
>   drivers/dpll/sit9531x/regs.h |  27 +++++
>   4 files changed, 298 insertions(+), 1 deletion(-)
> 
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index bba42fe302a3..8d857f1a0c89 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -1641,7 +1641,17 @@ int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
> 
>          sitdev->out[out_idx].freq = div64_u64(fvco, divo);
> 
> -       return 0;
> +       /*
> +        * The programmed reset delay counts VCO cycles against the output
> +        * period in force when it was written, so a rate change silently
> +        * re-times a previously requested phase adjust.  Re-encode the
> +        * cached picosecond request against the new rate.
> +        */
> +       if (sitdev->out[out_idx].phase_adj)
> +               rc = sit9531x_output_phase_adjust_set(sitdev, out_idx,
> +                                                     sitdev->out[out_idx].phase_adj);
> +
> +       return rc;
>   }
> 
>   /*
> @@ -1731,6 +1741,206 @@ int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
>    * 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, t_out_ps;
> +       s64 phase_norm_ps = 0;
> +       u8 page, base, prog6_val, fine = 0;
> +       u8 old_bytes[5], new_bytes[5], i;
> +       u8 pll_idx, slot;
> +       u64 freq;
> +       int rc, ret, rb_rc;
> +
> +       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 -EINVAL;
> +
> +       freq = sitdev->out[out_idx].freq;
> +       if (!freq) {
> +               /*
> +                * The cache is only seeded by a DT frequency list or an
> +                * earlier get/set; a board without supported-frequencies-hz
> +                * would otherwise get -EINVAL on every phase request forever.
> +                * Read the effective rate back from the divider chain.
> +                */
> +               rc = sit9531x_output_freq_get(sitdev, out_idx, &freq);
> +               if (rc)
> +                       return rc;
> +               if (!freq)
> +                       return -EINVAL;
> +       }
> +
> +       rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
> +       if (rc)
> +               return rc == -ENODATA ? -ENODEV : rc;
> +
> +       t_out_ps = div64_u64(1000000000000ULL, freq);
> +       if (!t_out_ps)
> +               return -EINVAL;
> +
> +       /*
> +        * Convert to unsigned absolute delay.  Both signs are folded modulo one
> +        * period: positive delays wrap naturally, negative delays are rendered as
> +        * T_out - |phase|.
> +        */
> +       if (phase_ps == 0) {
> +               abs_ps = 0;
> +       } else if (phase_ps > 0) {
> +               abs_ps = (u64)phase_ps;
> +               div64_u64_rem(abs_ps, t_out_ps, &abs_ps);
> +               phase_norm_ps = abs_ps;
> +       } else {
> +               u64 advance = (u64)(-(s64)phase_ps);
> +
> +               /*
> +                * div64_u64_rem() rather than the % operator: a 64-bit
> +                * modulo has no compiler helper on 32-bit targets and
> +                * leaves the module with an undefined __umoddi3.
> +                */
> +               div64_u64_rem(advance, t_out_ps, &advance);
> +               phase_norm_ps = -(s64)advance;
> +               abs_ps = (advance == 0) ? 0 : (t_out_ps - advance);
> +       }

This if-else branches could be reduced to:

<snip>
         abs_ps = abs(phase_ps); /* Safe. INT_MIN is not possible here*/

         div64_u64_rem(abs_ps, t_out_ps, &abs_ps);

         phase_norm_ps = phase_ps < 0 ? -(s64)abs_ps : (s64)abs_ps;
         abs_ps = phase_ps < 0 && abs_ps ? t_out_ps - abs_ps : abs_ps;
</snip>

> +
> +       /*
> +        * 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;
> +       }

Also this whole block can be skipped (and set coarse to 0) in case of
abs_ps == 0 to avoid expensive divisions.

Thanks,
Ivan


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

* Re: [PATCH net-next v9 02/15] dt-bindings: dpll: add SiTime SiT95316 clock generator
  2026-09-15  0:00 ` [PATCH net-next v9 02/15] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
@ 2026-09-17 15:01   ` netdev-bot+sashiko
  0 siblings, 0 replies; 33+ messages in thread
From: netdev-bot+sashiko @ 2026-09-17 15:01 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree,
	netdev, linux-kernel, kuba

Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider:

Critical: 0 · High: 0 · Medium: 0 · Low: 1

- [Low] The binding asserts fixed hardware counts (four PLLs PLLA..PLLD,
  8 individually selectable inputs, 8 outputs on SiT95317 / 12 on…

--

dt-bindings: dpll: add SiTime SiT95316 clock generator

Adds a YAML binding for the SiTime SiT9531x family (SiT95316/SiT95317)
of I2C-controlled clock generators described as DPLL providers, with
four PLLs, up to eight inputs and up to twelve outputs.  Also adds a
MAINTAINERS entry for the new binding.

> diff --git a/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml b/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
> new file mode 100644
> index 0000000000000..e71ee3611a830
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
> @@ -0,0 +1,181 @@

[ ... ]

> +  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.

[ ... ]

> +  sitime,pll-fvco:
> +    $ref: /schemas/types.yaml#/definitions/uint64-array
> +    minItems: 4
> +    maxItems: 4

[ ... ]

> +  sitime,output-pll-map:
> +    $ref: /schemas/types.yaml#/definitions/uint32-array
> +    minItems: 8
> +    maxItems: 12

[ ... ]

> +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

[Severity: Low]
This isn't a bug in the strict sense, but the per-variant bounds here only
constrain the vendor properties, while the properties inherited from
dpll-device.yaml that describe the same hardware stay unbounded.  Was that
intentional?

The description states 4 PLLs, 8 selectable inputs, and 8 outputs
(SiT95317) or 12 outputs (SiT95316).  sitime,pll-fvco is pinned to exactly
4 entries and sitime,output-pll-map is bounded per variant, but
dpll-types in dpll-device.yaml has no length constraint:

  dpll-types:
    description: List of DPLL channel types, one per DPLL instance.
    $ref: /schemas/types.yaml#/definitions/non-unique-string-array
    items:
      enum: [pps, eec]

so a node for this 4-PLL part can list five or more channel types and still
pass dt_binding_check.  Would adding dpll-types with maxItems: 4 here make
that match the hardware?

Similarly, pin subnodes come from dpll-pin.yaml, which deliberately leaves
the hardware index open:

  reg:
    description: Hardware index of the DPLL pin.
    maxItems: 1

With no per-variant maximum, input-pins/pin@9 validates cleanly even though
only 8 inputs are individually selectable, and a SiT95317 node can declare
output-pins/pin@8 while the same DTS is limited to an 8-entry
sitime,output-pll-map, i.e. the two descriptions can silently disagree.
Since sit9531x_prop_pin_fwnode_get() in drivers/dpll/sit9531x/prop.c looks
up the child whose reg matches a driver-generated hardware index, an
out-of-range pin node is silently ignored rather than rejected, so a
mistyped index gives no diagnostic.  Would per-variant reg maxima on the
input-pins and output-pins children be worth adding alongside the
output-pll-map constraints?

For context, the already merged microchip,zl30731.yaml (5 channels, 10
inputs, 20 outputs) is unbounded in the same way, so this looks like
existing looseness in the dpll bindings rather than something specific to
this patch.

[ ... ]

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260915000015.80480-1-arouhi%40sitime.com

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

* Re: [PATCH net-next v9 03/15] dpll: add basic SiTime SiT9531x support
  2026-09-15  0:00 ` [PATCH net-next v9 03/15] dpll: add basic SiTime SiT9531x support Ali Rouhi
  2026-09-17  8:42   ` Ivan Vecera
@ 2026-09-17 15:01   ` netdev-bot+sashiko
  1 sibling, 0 replies; 33+ messages in thread
From: netdev-bot+sashiko @ 2026-09-17 15:01 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree,
	netdev, linux-kernel, kuba

Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider:

Critical: 0 · High: 0 · Medium: 0 · Low: 1

- [Low] The user-visible Kconfig help for SIT9531X_DPLL states the driver
  "registers each on-chip PLL as a DPLL device and exposes input/output…

--

dpll: add basic SiTime SiT9531x support

Adds the first piece of a SiTime SiT9531x driver: an I2C driver with a
paged regmap configuration, page/offset register helpers, XO rate
lookup, an optional reset line and variant detection from the ID byte
at page 0 offset 0x02.  No DPLL objects are registered yet.

> diff --git a/drivers/dpll/sit9531x/Kconfig b/drivers/dpll/sit9531x/Kconfig
> new file mode 100644
> index 0000000000000..47aea8674327c
> --- /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.

[Severity: Low]
This isn't a bug, but the help text describes behaviour that does not
exist yet at this commit.  Nothing in this patch includes a DPLL header
or calls a dpll_*() function, and select DPLL pulls the DPLL core in for
a module that does not use it.

Would it be worth moving the "registers each on-chip PLL as a DPLL
device ... via Generic Netlink" paragraph and select DPLL to the patch
that actually performs the registration, so the visible config option
matches the code at every step of the series?

For the record, this resolves later in the series: the final tree adds
dpll.c and prop.c, sit9531x_dpll_device_register() calls
dpll_device_register(), and sit9531x_dev_probe() ends with
sit9531x_devm_dpll_init(), which makes the help text accurate again.

> +
> +	  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 0000000000000..a221fe55386a9
> --- /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
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> new file mode 100644
> index 0000000000000..b0920f76362a1
> --- /dev/null
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -0,0 +1,272 @@

[ ... ]

> +	dev_info(sitdev->dev, "%s detected, %u inputs, %u outputs\n",
> +		 sitdev->info->name, sitdev->info->num_inputs,
> +		 sitdev->info->num_outputs);
> +
> +	return 0;
> +}

Related to the Kconfig comment above: sit9531x_dev_probe() returns right
after the dev_info(), so a device tree node describing dpll-types,
input-pins and output-pins binds successfully with no DPLL device and no
netlink interface, and without any message saying so.  Is a note in the
help text or the commit log about this intermediate state wanted?

[ ... ]

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260915000015.80480-1-arouhi%40sitime.com

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

* Re: [PATCH net-next v9 04/15] dpll: sit9531x: read DPLL types and pin properties from system firmware
  2026-09-15  0:00 ` [PATCH net-next v9 04/15] dpll: sit9531x: read DPLL types and pin properties from system firmware Ali Rouhi
  2026-09-17  9:42   ` Ivan Vecera
@ 2026-09-17 15:01   ` netdev-bot+sashiko
  1 sibling, 0 replies; 33+ messages in thread
From: netdev-bot+sashiko @ 2026-09-17 15:01 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree,
	netdev, linux-kernel, kuba

Thank you for your contribution! Sashiko AI review found 4 potential
issue(s) to consider:

Critical: 0 · High: 0 · Medium: 3 · Low: 1

- [Medium] In `sit9531x_pin_props_get()` (drivers/dpll/sit9531x/prop.c,
  output branch) every output pin is advertised to the DPLL core with…
- [Medium] `sit9531x_pin_props_get()` (drivers/dpll/sit9531x/prop.c)
  reinterprets the first entry of the DT property `supported-…
- [Medium] `sit9531x_prop_pin_package_label_set()` builds the DPLL
  package label for outputs as `snprintf(..., "OUT%u", id)` from the…
- [Low] In `sit9531x_pin_props_get()` the firmware frequency is held in
  `u64 curr_freq` (assigned `curr_freq = freqs[0]` straight from…

--

dpll: sit9531x: read DPLL types and pin properties from system firmware

This patch adds prop.c, which builds the per-pin dpll_pin_properties
(package label, type, capabilities, supported frequencies) and the per-channel
DPLL type from the firmware node, with defaults when the node is absent.  It
also adds the per-input and per-output runtime state arrays to struct
sit9531x_dev.

A few questions about the property parsing below.

> diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
> index 76a2632f0ce4a..230b21b9e2381 100644
> --- a/drivers/dpll/sit9531x/core.h
> +++ b/drivers/dpll/sit9531x/core.h

[ ... ]

> @@ -41,6 +51,39 @@ struct sit9531x_chip_info {
>  	const u8	*clkout_map;
>  };
>  
> +/*
> + * struct sit9531x_ref - input reference state
> + * @freq:		configured frequency in Hz
> + * @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;
> +	const char			*label;
> +	enum sit9531x_signal_mode	sig_mode;
> +};
> +
> +/*
> + * struct sit9531x_out - output state
> + * @freq:		configured frequency in Hz
> + * @label:		board label from DT or default
> + */
> +struct sit9531x_out {
> +	u32		freq;
> +	const char	*label;
> +};
> +

[ ... ]

> diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
> new file mode 100644
> index 0000000000000..c9642b702c4ab
> --- /dev/null
> +++ b/drivers/dpll/sit9531x/prop.c
> @@ -0,0 +1,437 @@

[ ... ]

> +	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);

[Severity: Medium]
Should the output label go through sitdev->info->clkout_map[] rather than the
logical index?

drivers/dpll/sit9531x/core.c has a non-identity map for one variant:

    static const u8 clkout_map_95317[] = {0, 3, 4, 5, 7, 8, 9, 11};

and core.h documents it as "@clkout_map: per-output slot mapping (output
index -> physical slot)", with sit9531x_output_divo_write() using it for the
actual register access.  On SiT95317 that makes logical index 1 program
physical slot 3, but userspace is shown DPLL_A_PIN_PACKAGE_LABEL "OUT1", and
"OUT10"/"OUT11" are never emitted even though those chip pins exist.

The same logical index is used as the firmware lookup key in
sit9531x_prop_pin_fwnode_get():

    if (fwnode_property_read_u32(pin_node, "reg", &reg))
            continue;

    if (id == reg)
            break;

while Documentation/devicetree/bindings/dpll/dpll-pin.yaml describes reg as
"Hardware index of the DPLL pin".  A board DT written against the binding
(pin@3, pin@7, pin@b) then matches nothing, and pin@1 has its label,
connection-type, esync-control and frequency list applied to physical slot 3.

Should prop.c translate through clkout_map, or should the sitime binding
state that reg is a logical, variant-dependent index?

[ ... ]

> +	} else {
> +		/*
> +		 * A synthesized clock output is an external connection with
> +		 * no more specific meaning; a board that knows better says
> +		 * so through the pin's connection-type property below.
> +		 */
> +		props->dpll_props.type = DPLL_PIN_TYPE_EXT;
> +		props->dpll_props.capabilities =
> +			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
> +		curr_freq = sitdev->out[index].freq;
> +
> +		/*
> +		 * Allow phase-adjust over a +/-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.  Only outputs get a range: input pins
> +		 * have no .phase_adjust_set, and advertising one there would
> +		 * promise userspace something every set would refuse.
> +		 */
> +		props->dpll_props.phase_range.min = -1000000000; /* -1 ms in ps */
> +		props->dpll_props.phase_range.max =  1000000000; /* +1 ms in ps */
> +		/*
> +		 * The fine step is 30 ps, but requests are accepted at 1 ps
> +		 * resolution and rounded to the nearest achievable delay, so
> +		 * advertise the request granularity, not the hardware step.
> +		 */
> +		props->dpll_props.phase_gran = 1;
> +	}

[Severity: Medium]
Can these advertised values mislead userspace, given they are uAPI once
released?

dpll_pin_phase_adj_set() in drivers/dpll/dpll_netlink.c validates a request
only against the driver-supplied range and granularity:

    if (phase_adj > pin->prop.phase_range.max ||
        phase_adj < pin->prop.phase_range.min) {

So every integer-picosecond request up to 1 ms is accepted, and
sit9531x_output_phase_adjust_set() then folds an offset larger than one
output period back into a single period and rounds to the 30 ps step.  A
request of 500000 ps on a 1 GHz output is applied as a few hundred ps and
phase_adjust_get() reports that different value back.

Documentation/netlink/specs/dpll.yaml describes
DPLL_A_PIN_PHASE_ADJUST_GRAN as "The value of phase adjustment must be a
multiple of this granularity", so is phase_gran = 1 the right thing to
publish when the real step is 30 ps?

Related, further down in the same function:

    if (dir == DPLL_PIN_DIRECTION_OUTPUT && num_freqs == 0) {
            ranges[j].min = 1;
            ranges[j].max = 1000000000ULL; /* 1 GHz */

For an output whose firmware node lists no supported-frequencies-hz this
claims 1 Hz to 1 GHz and effectively disables the core-side frequency_set
validation, so the advertised capability follows DT completeness rather than
the silicon.  For comparison, zl3073x derives phase_gran from the synth rate
and filters DT frequencies through zl3073x_pin_check_freq().  The commit
message does not mention either the phase-adjust window or this fallback
range; could it describe them?

[ ... ]

> +	/*
> +	 * 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 ||
> +		    index != SIT9531X_MAX_INPUTS)
> +			curr_freq = freqs[0];
> +	}

[Severity: Medium]
Is element 0 of supported-frequencies-hz allowed to mean "current
frequency"?

Documentation/devicetree/bindings/dpll/dpll-pin.yaml defines the property as:

    supported-frequencies-hz:
        description: List of supported frequencies for this pin, expressed in Hz.

with no special meaning for the first entry.  The other consumer of the same
binding, drivers/dpll/zl3073x/prop.c, reads curr_freq from the hardware and
uses the DT list only to build freq_supported:

    /* Start with current frequency at index 0 */
    ranges[0] = (struct dpll_pin_frequency)DPLL_PIN_FREQUENCY(curr_freq);

For a board that legitimately lists several supported frequencies, this
invents a current rate.  The cached value is then reported verbatim by
sit9531x_dpll_input_pin_frequency_get() and used as the output period in
sit9531x_output_phase_adjust_set() ("freq = sitdev->out[out_idx].freq;"), so
a seeded rate that differs from what the chip is generating yields a wrong
programmed delay.

> +skip_fwnode_props:

[ ... ]

> +	/*
> +	 * Publish the seeded rate only once the pin is certain to be built.
> +	 * The allocation above is the last thing that can fail, and a call
> +	 * that reports failure must not leave the device's cached rate
> +	 * changed behind it.
> +	 */
> +	if (curr_freq) {
> +		if (dir == DPLL_PIN_DIRECTION_INPUT)
> +			sitdev->ref[index].freq = curr_freq;
> +		else
> +			sitdev->out[index].freq = curr_freq;
> +	}

[Severity: Low]
Does this truncate curr_freq?

curr_freq is a u64 taken straight from fwnode_property_read_u64_array() and
published at full width through DPLL_PIN_FREQUENCY(), but sit9531x_ref.freq
and sit9531x_out.freq are u32 in core.h.  A firmware entry of 2^32 Hz or more
is stored truncated while the advertised freq_supported entry keeps the full
value, so the cached rate matches nothing in the advertised set; an exact
multiple of 2^32 caches as 0, which also affects the period used by
sit9531x_output_phase_adjust_set().

zl3073x_pin_props_get() runs every DT entry through
zl3073x_pin_check_freq(); would a similar bound check fit here?

> +
> +	j = 0;

[ ... ]

> +	if (dir == DPLL_PIN_DIRECTION_OUTPUT && num_freqs == 0) {
> +		ranges[j].min = 1;
> +		ranges[j].max = 1000000000ULL; /* 1 GHz */
> +		j++;
> +	}

[ ... ]

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260915000015.80480-1-arouhi%40sitime.com

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

* Re: [PATCH net-next v9 05/15] dpll: sit9531x: register DPLL devices and pins
  2026-09-15  0:00 ` [PATCH net-next v9 05/15] dpll: sit9531x: register DPLL devices and pins Ali Rouhi
@ 2026-09-17 15:01   ` netdev-bot+sashiko
  0 siblings, 0 replies; 33+ messages in thread
From: netdev-bot+sashiko @ 2026-09-17 15:01 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree,
	netdev, linux-kernel, kuba

Thank you for your contribution! Sashiko AI review found 8 potential
issue(s) to consider:

Critical: 0 · High: 0 · Medium: 4 · Low: 4

- [Medium] Commit message claims pins are registered that the code cannot
  register.
- [Medium] sit9531x_dpll_mode_set() can never be invoked through the DPLL
  netlink API, so the hardware transition it implements (clearing PLL…
- [Medium] Changes to the exported lock-status error are never notified.
- [Medium] sit9531x_irq_thread_fn() returns IRQ_NONE when the I2C write
  that acknowledges the chip's write-1-to-clear notification latches…
- [Low] core.h declares 16 driver-internal functions that no translation
  unit defines at this commit (sit9531x_input_disable/enable,…
- [Low] The in-tree binding
  Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml documents…
- [Low] The commit message states the kthread worker "polls the
  interesting registers twice a second into a per-input, per-output and…
- [Low] The rewritten descriptive comments for struct sit9531x_dev and
  struct sit9531x_out drop entries for fields that still exist and omit…

--

dpll: sit9531x: register DPLL devices and pins

This adds a DPLL registration layer to the SiT9531x driver: each of the
four on-chip PLLs becomes a DPLL device, a kthread worker polls status
registers twice a second into a cache that the callbacks answer from, and
an optional INTRB interrupt kicks that same tick.  A clock_id is derived
from the variant, the I2C adapter number and the client address.

A few questions below, mostly about the changelog and about paths that
cannot be reached at this commit.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index b0920f76362a1..6dfb1b31d4913 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c

[ ... ]

> @@ -145,122 +159,1597 @@ int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
>  	return regmap_update_bits(sitdev->regmap, reg, mask, val);
>  }
>  

[ ... ]

> +	/*
> +	 * 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;
> +	}

[Severity: Low]
Is out_pll_map_valid ever true at this commit?  Nothing in
drivers/dpll/sit9531x/ reads "sitime,output-pll-map" or "sitime,pll-fvco"
yet, so this branch is dead code and pll_fvco[] stays zero.

A board written to the properties documented in
Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml is therefore
ignored, while struct sit9531x_dev describes both fields as coming from DT.

The parser does land later in the series, in the patch that lets the device
tree override the two board facts, so this looks like the consumer and the
SIT9531X_OUT_PLL_UNMAPPED define arriving one patch ahead of their producer.

[ ... ]

> +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;
> +

[ ... ]

> +	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);

[Severity: Low]
The changelog says the worker "polls the interesting registers twice a
second into a per-input, per-output and per-PLL cache".  Where does the
per-output part happen?

sit9531x_dev_periodic_work() calls only sit9531x_dev_ref_states_update()
and sit9531x_dev_chan_states_update().  out[] is filled once by
sit9531x_dev_state_fetch() during probe and never refreshed, so
out->enabled, out->cmos and out->routed keep their probe-time values.

By the end of the series this looks intentional -- the output pin state
getter reads through on a state_stale flag, with a comment saying there is
no poll of output state to correct it -- so perhaps only the changelog
sentence needs adjusting.

[ ... ]

> +static irqreturn_t sit9531x_irq_thread_fn(int irq, void *data)
> +{
> +	struct sit9531x_dev *sitdev = data;
> +	int rc;
> +

[ ... ]

> +	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);
> +		/*
> +		 * The latch was not acknowledged, so with IRQF_ONESHOT the
> +		 * still-asserted line would re-enter this handler back to
> +		 * back forever.  Report the interrupt as unhandled so the
> +		 * spurious-IRQ detector can engage if the bus stays broken.
> +		 */
> +		return IRQ_NONE;
> +	}

[Severity: Medium]
Can this loop without bound?  With a level-triggered INTRB and IRQF_ONESHOT
the line is unmasked when the threaded handler returns, whatever it returns.

If sit9531x_clear_notifications() keeps failing (bus NAK, arbitration loss,
adapter error) the write-1-to-clear latch stays set, INTRB stays asserted,
and sit9531x_irq_thread_fn() is re-entered back to back.  Each pass takes
sitdev->multiop_lock and issues failing paged I2C transfers from a
SCHED_FIFO thread, contending the lock with the poll worker and with every
netlink callback.

The comment names the spurious-IRQ detector as the bound, but
note_interrupt() only disables a line after roughly 100000 interrupts with
nearly all of them unhandled, and it resets irqs_unhandled once more than
HZ/10 has passed since the last unhandled one.  With multi-millisecond I2C
timeouts per pass, is that threshold reachable?

Would disable_irq_nosync() on this path, or masking the interrupt at the
device, be a firmer stop?  The code is unchanged at the end of the series.

[ ... ]

> +static bool sit9531x_dpll_pin_is_registrable(struct sit9531x_dpll *sitdpll,
> +					     enum dpll_pin_direction dir,
> +					     u8 index)
> +{
> +	/*
> +	 * Only the XO pin has a complete pin-op table in this patch.
> +	 * Other pin classes are registered once their state callbacks
> +	 * land in the following patches.
> +	 */
> +	if (dir != DPLL_PIN_DIRECTION_INPUT)
> +		return false;
> +
> +	return index == SIT9531X_MAX_INPUTS;
> +}

[Severity: Medium]
The changelog opens with:

  "Register each of the four on-chip PLLs as a DPLL device and the clocks
   around them as pins: the inputs, the crystal, and the outputs the loaded
   configuration routes to a PLL."

Does that match this filter?  Every output returns false, and for inputs
only index == SIT9531X_MAX_INPUTS is accepted, i.e. the crystal pin alone,
so userspace sees four DPLL devices each carrying exactly one pin.

The two new ops tables in dpll.c supply only .direction_get:

	static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
		.direction_get		= sit9531x_dpll_input_pin_direction_get,
	};

and dpll_pin_register() in drivers/dpll/dpll_core.c refuses a table without
a state getter:

	if (WARN_ON(!ops) ||
	    WARN_ON(!ops->state_on_dpll_get) ||
	    WARN_ON(!ops->direction_get) ...
		return -EINVAL;

so the input and output pins the paragraph describes could not be registered
even if the filter allowed them.

sit9531x_dpll_register()'s own comment says it registers "all input and
output pins that are connected to this PLL", and sit9531x_dpll_changes_check()
skips the one pin class that is registered, so no pin-change notification
can fire here either.  Could the first paragraph say that only the crystal
pin is registered at this step?

[ ... ]

> diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
> index 230b21b9e2381..5ead3811e14b4 100644
> --- a/drivers/dpll/sit9531x/core.h
> +++ b/drivers/dpll/sit9531x/core.h

[ ... ]

> @@ -64,37 +77,97 @@ enum sit9531x_signal_mode {

[ ... ]

>  /*
>   * struct sit9531x_out - output state
> - * @freq:		configured frequency in Hz
> + * @enabled:		output is driving, i.e. not forced into Hi-Z
> + * @cmos:		output is wired single-ended; the Hi-Z pair that
> + *			speaks for it is the SE one, not the differential
> + * @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;
> +	u64		freq;
> +	bool		enabled;
> +	bool		cmos;
> +	bool		routed;
> +	u8		pll_idx;
>  	const char	*label;
>  };

[ ... ]

>  /*
>   * struct sit9531x_dev - SiT9531x device instance
> - * @dev:		parent device
> - * @client:		I2C client
> - * @regmap:		paged register map
>   * @info:		detected chip variant info
> - * @multiop_lock:	serializes multi-register sequences
> + * @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

[Severity: Low]
These rewritten comment blocks drop entries for members that are still
present, and miss a new one.

struct sit9531x_dev loses @dev, @client and @regmap while all three members
remain, and the newly added "struct list_head dplls;" is not documented.
struct sit9531x_out loses @freq while the (widened to u64) freq member
remains.

Both blocks open with "/*" rather than "/**", so kernel-doc will not report
this, and the drift is still there at the end of the series.

> @@ -107,11 +180,107 @@ struct sit9531x_dev {

[ ... ]

> +/* ---- 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);

[ ... ]

> +/* ---- 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);

[Severity: Low]
Sixteen of the prototypes added here have no definition in the tree at this
commit: sit9531x_input_disable/enable, the four prio helpers,
sit9531x_output_disable/enable, sit9531x_output_freq_set/get,
sit9531x_output_phase_adjust_set, sit9531x_intsync_enable/disable,
sit9531x_output_pulse_ctrl_set, sit9531x_pll_ffo_ppt and
sit9531x_phase_offset_read.

core.c also refers to them: sit9531x_output_forced_hiz()'s comment says
"mirroring what sit9531x_output_disable() programs", and
sit9531x_ref_state_fetch() says "until something called
sit9531x_input_enable()".

All of them are defined by the end of the series, so nothing fails to link;
would declaring each in the patch that defines it be tidier?

> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> new file mode 100644
> index 0000000000000..b4984335e6bfc
> --- /dev/null
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -0,0 +1,444 @@

[ ... ]

> +static int
> +sit9531x_dpll_mode_get(const struct dpll_device *dpll, void *dpll_priv,
> +		       enum dpll_mode *mode, struct netlink_ext_ack *extack)
> +{
> +	*mode = DPLL_MODE_AUTOMATIC;
> +
> +	return 0;
> +}

[Severity: Medium]
Can sit9531x_dpll_mode_set() ever be reached?  mode_get always reports
AUTOMATIC, whatever the cached chan->mode free-run state says, and
dpll_mode_set() in drivers/dpll/dpll_netlink.c does:

	ret = ops->mode_get(...);
	...
	if (mode == old_mode)
		return 0;

so an AUTOMATIC request returns before ops->mode_set, and every other value
is rejected with -EINVAL because supported_modes_get() advertises only
AUTOMATIC.

That leaves the clearing of PLL page reg 0x31[5], and its careful
restore-on-latch-failure path, unreachable, so a PLL left free-running by
the loaded configuration or by the vendor tool cannot be put back into
priority-based selection through the only interface the driver exposes.
mode_get is unchanged at the end of the series -- should it report the
free-run state so the setter becomes reachable?

[ ... ]

> +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);
> +	}

[Severity: Medium]
This passes NULL for status_error and compares only the primary status
against sitdpll->lock_status.

chan->inner_lol is refreshed independently by sit9531x_chan_state_fetch(),
and sit9531x_dpll_lock_status_get() turns it into
DPLL_LOCK_STATUS_ERROR_UNDEFINED.  The core exports that value:

	if (status_error &&
	    (status == DPLL_LOCK_STATUS_UNLOCKED ||
	     status == DPLL_LOCK_STATUS_HOLDOVER) &&
	    nla_put_u32(msg, DPLL_A_LOCK_STATUS_ERROR, status_error))

So when inner loss of lock appears or clears while the primary status stays
UNLOCKED or HOLDOVER, what calls dpll_device_change_ntf()?  Subscribers
would keep the stale error until an unrelated status change or an explicit
GET.  This is unchanged at the end of the series.

[ ... ]

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260915000015.80480-1-arouhi%40sitime.com

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

* Re: [PATCH net-next v9 06/15] dpll: sit9531x: implement input pin state on a DPLL
  2026-09-15  0:00 ` [PATCH net-next v9 06/15] dpll: sit9531x: implement input pin state on a DPLL Ali Rouhi
@ 2026-09-17 15:01   ` netdev-bot+sashiko
  0 siblings, 0 replies; 33+ messages in thread
From: netdev-bot+sashiko @ 2026-09-17 15:01 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree,
	netdev, linux-kernel, kuba

Thank you for your contribution! Sashiko AI review found 9 potential
issue(s) to consider:

Critical: 0 · High: 1 · Medium: 4 · Low: 4

- [High] sit9531x_prio_table_commit() publishes the *intended* table into
  the shared chan->prio_mask cache even when the slot writes failed…
- [Medium] Inconsistent source-code canonicalization between the
  producers of the cached hardware state and the new pin-facing…
- [Medium] The SELECTABLE branch of
  sit9531x_dpll_input_pin_state_on_dpll_set() keys the shared-receiver…
- [Medium] sit9531x_input_prio_set() locates only the *first* occurrence
  of the requested source and then shifts the intervening entries, so…
- [Medium] On a mid-sequence read or write failure,
  sit9531x_prio_table_commit() jumps to `commit:` and unconditionally…
- [Low] sit9531x_dpll_pin_is_registrable()'s rewritten kernel-doc claims
  "For output pins: the pin is registrable if this DPLL is routed to it,…
- [Low] prop.c advertises DPLL_PIN_CAPABILITIES_PRIORITY_CAN_CHANGE for
  every physical input pin, but sit9531x_dpll_input_pin_ops — the ops…
- [Low] sit9531x_input_disable()'s comment claims that attempting both
  register operations means "a half-programmed override is not left…
- [Low] sit9531x_input_prio_add() only ever overwrites a slot whose
  canonical source is *duplicated* (`if (seen[src_canon] > 1)`), and…

--

dpll: sit9531x: implement input pin state on a DPLL

This adds state_on_dpll_get/set for input pins, backed by new helpers that
read, rewrite and latch a PLL's priority table under forced holdover, plus
force/state register helpers that gate the shared input receiver.  Input
pins other than the crystal now become registrable.

A few questions about the new code below.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index 6dfb1b31d4913..25418485e9757 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -185,6 +185,93 @@ static void sit9531x_input_get_regs(const struct sit9531x_dev *sitdev,
>  	}
>  }
>  
> +/*
> + * 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.
> + *
> + * Both writes are attempted even when the first fails, and the first
> + * error is returned: the force and state bits only mean something
> + * together, so a half-programmed override is not left behind on a
> + * transient bus error.
> + *
> + * Caller must hold sitdev->multiop_lock.
> + */
> +int sit9531x_input_disable(struct sit9531x_dev *sitdev, u8 index)
> +{
[ ... ]
> +	rc = sit9531x_read_u8(sitdev, force_reg, &val);
> +	if (!rc)
> +		rc = sit9531x_write_u8(sitdev, force_reg, val | BIT(pair));
> +
> +	ret = sit9531x_read_u8(sitdev, state_reg, &val);
> +	if (!ret)
> +		ret = sit9531x_write_u8(sitdev, state_reg, val & ~BIT(pair));
> +	if (ret && !rc)
> +		rc = ret;
> +
> +	if (rc)
> +		return rc;

[Severity: Low]
Does the comment above match what the code does?  The claim is that "a
half-programmed override is not left behind on a transient bus error", but
these are two independent read-modify-writes with no rollback and no
verification.

If the force_reg write succeeds and the state_reg access then fails, the
force bit is set while the old state bit remains; when that bit reads 1 the
override forces the receiver on rather than off.  If the force_reg access
fails and the state_reg write succeeds, the inverse partial update stays in
the device.  Returning the first error restores neither register.

Would it be more accurate to say that both operations are attempted and the
first error reported, without promising that no partial override remains?

[ ... ]

> @@ -347,6 +434,92 @@ static u8 sit9531x_prio_slot_get(u8 val, u8 slot)
>  	return val >> SIT9531X_PRIO_HI_SHIFT;
>  }
>  
[ ... ]
> +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;
> +}
> +
> +/*
> + * Fold a source code to the lane a DPLL pin actually represents.
> + *
> + * Differential input pairs expose only the P lane as a DPLL pin.  A
> + * priority table entry encoded as an N lane for such a pair must map to
> + * the P-lane source for pin-facing operations (membership, priority slots,
> + * add/remove/set lookups), matching sit9531x_ref_pll_mask_fetch().
> + */
> +static u8 sit9531x_prio_src_canon(const struct sit9531x_dev *sitdev, u8 src)
> +{
> +	u8 index = sit9531x_hw_src_input(src);
> +
> +	if (index >= sitdev->info->num_inputs)
> +		return src;
> +
> +	if (sit9531x_input_is_n(index) &&
> +	    sitdev->ref[index].sig_mode == SIT9531X_MODE_DE)
> +		return sit9531x_input_hw_src(index - 1);
> +
> +	return src;
> +}
> +
> +/*
> + * sit9531x_input_prio_present - is a source listed in a PLL's priority table
> + * @input_idx:	input source in hardware encoding (see
> + *		sit9531x_input_hw_src())
> + *
> + * Answers from the membership mask that every table write and every poll
> + * refreshes, which is what the pin state getters test.  The priority slot
> + * cannot answer this: a source that is not in the table reports the lowest
> + * slot, so the slot value alone does not separate absent from last.
> + *
> + * Caller must hold sitdev->multiop_lock.
> + */
> +bool sit9531x_input_prio_present(struct sit9531x_dev *sitdev, u8 pll_idx,
> +				 u8 input_idx)
> +{
> +	lockdep_assert_held(&sitdev->multiop_lock);
> +
> +	if (pll_idx >= SIT9531X_NUM_PLLS)
> +		return false;
> +
> +	input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
> +	if (input_idx >= SIT9531X_PRIO_NUM_SRC)
> +		return false;
> +
> +	return !!(sitdev->chan[pll_idx].prio_mask & BIT(input_idx));
> +}
> +
>  /*
>   * Rebuild a PLL's membership mask from the source codes of its priority
>   * table.  The mask is what the pin state getters test, so it is refreshed
> @@ -365,6 +538,90 @@ static void sit9531x_prio_mask_build(struct sit9531x_dev *sitdev, u8 pll_idx,
>  	sitdev->chan[pll_idx].prio_mask = mask;
>  }
>  
> +/* Attempts to release a forced holdover before reporting it stuck. */
> +#define SIT9531X_HO_CLEAR_TRIES		3
> +
[ ... ]
> +static int sit9531x_prio_table_commit(struct sit9531x_dev *sitdev, u8 pll_idx,
> +				      const u8 *srcs)
> +{
[ ... ]
> +	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: slots written before a failed write are in
> +	 * the table regardless, so the latch keeps hardware and the mask
> +	 * rebuild below consistent with what was actually written.
> +	 */
> +	prg_rc = sit9531x_prio_prg_commit(sitdev);
> +	if (prg_rc && !rc)
> +		rc = prg_rc;

[Severity: Medium]
On a failed slot read or write, this jumps to commit: and issues the
small-change update directive anyway, then clears HO_FORCE below.  Does that
latch a table that is a mix of old and new slots into the running reference
selection loop?

The sequence on an I2C error after the first slot was written is:

	sit9531x_write_u8(slot N) fails
	goto commit
	sit9531x_prio_prg_commit()   /* SIT9531X_SMALL_UPDATE_CMD */
	... HO_FORCE cleared ...

so the loop resumes on a priority order that is neither the previous one nor
the requested one.

The old nibble is read into val only to be overwritten, so nothing is kept
for a restore, even though every caller still holds the pre-write table
(orig_srcs in sit9531x_input_prio_set(), the freshly read srcs[] in
sit9531x_input_prio_add() and sit9531x_input_prio_remove()).  Could the
saved nibbles be written back on failure, the way
sit9531x_output_divo_write() keeps old_bytes[] and rolls back?

> +
> +	/*
> +	 * Refresh the mask from the table just written so a get that follows
> +	 * a set does not have to wait for the next poll.  Slots written
> +	 * before a failed write are in the table too, so this is closer to
> +	 * the truth than the pre-write mask either way, and the poll read-back
> +	 * corrects whatever a partial write left behind.
> +	 */
> +	sit9531x_prio_mask_build(sitdev, pll_idx, srcs);

[Severity: High]
This publishes the intended table into chan->prio_mask even when the slot
writes failed.  Does that make the membership re-check in the pin state
setter tautological?

The DISCONNECTED path in sit9531x_dpll_input_pin_state_on_dpll_set() does:

	rc = sit9531x_input_prio_remove(sitdev, sitdpll->id, hw_src);
	if (rc && sit9531x_input_prio_present(sitdev, sitdpll->id,
					      hw_src))
		break;
	ref->pll_mask &= ~pll_bit;

but sit9531x_input_prio_present() only tests the mask this function just
overwrote with the requested image, so it reports the source absent no
matter what the chip's table now holds, and ref->pll_mask loses this DPLL.

The poll worker's sit9531x_chan_state_fetch() later re-reads the real table
into chan->prio_mask, so the getter goes back to reporting the input
SELECTABLE on that PLL, but ref->pll_mask is not re-derived:
sit9531x_ref_pll_mask_fetch() has one caller, sit9531x_dev_state_fetch(),
which runs once at probe.

Can the refcount then reach zero while a PLL still lists the input?

	PLL A disconnect, slot write fails -> ref->pll_mask loses bit A
	  (chip table may still list the source)
	PLL B disconnect succeeds -> ref->pll_mask == 0
	  -> sit9531x_input_disable(IN0)

which would force off a shared receiver that PLL A still lists and may be
locked to.  The commit message says "the poll read-back corrects whatever a
partial write left behind"; that holds for chan->prio_mask, but does anything
correct ref->pll_mask?

Would it work better to have sit9531x_prio_table_commit() rebuild the mask
from a read-back of the table it just wrote, and to re-derive ref->pll_mask
from the tables of all PLLs in the poll path?

> +
> +	/*
> +	 * Release the forced holdover.  Nothing else in the driver clears
> +	 * this bit, so a PLL left with it set reports holdover until the
> +	 * next table write on the same PLL happens to clear it, which may
> +	 * never come.  Retry before giving up, and say so if it stays set.
> +	 */
[ ... ]
> @@ -389,6 +646,208 @@ static int sit9531x_prio_table_read(struct sit9531x_dev *sitdev, u8 pll_idx,
>  	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.
[ ... ]
> + */
> +int sit9531x_input_prio_set(struct sit9531x_dev *sitdev, u8 pll_idx,
> +			    u8 input_idx, u8 prio)
> +{
[ ... ]
> +	for (from = 0; from < SIT9531X_PRIO_MAX_SLOTS; from++)
> +		if (sit9531x_prio_src_canon(sitdev, srcs[from]) == input_idx)
> +			break;
> +
> +	if (from == SIT9531X_PRIO_MAX_SLOTS)
> +		return -EINVAL;
> +
> +	if (from == prio)
> +		return 0;
> +
> +	if (from > prio) {
> +		/* Moving up: push the entries in between down one slot. */
> +		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;
> +	if (!memcmp(srcs, orig_srcs, sizeof(srcs)))
> +		return 0;

[Severity: Medium]
Only the first occurrence of the source is located, so what happens when the
table holds the same source more than once?

With [A,B,C,C,C,...] and a request to put C at slot 4, from is 2, the shift
copies C over C, and the result is identical to orig_srcs, so the memcmp
shortcut returns 0 without any register write.

With [A,B,C,D,C] and a request for slot 4, the result is [A,B,D,C,C], which
leaves C effectively at slot 3.

Duplicates are a normal state here because sit9531x_input_prio_remove()
backfills the freed tail slots with the lowest-priority remaining source
(kept[count] = kept[count - 1]), so a disconnect on a PLL creates them.

A later read via sit9531x_input_prio_get() reports the first occurrence from
chan->prio_last[], so it would contradict the value that was just accepted
with a 0 return.  Should the last occurrence be used when moving a source
down, or duplicates be collapsed before the shift?

This helper has no caller in this patch; the following commit "dpll: sit9531x:
add support to get and set priority on input pins" wires it to .prio_set, and
the logic is unchanged at the end of the series.

> +
> +	return sit9531x_prio_table_commit(sitdev, pll_idx, srcs);
> +}
> +
[ ... ]
> +int sit9531x_input_prio_remove(struct sit9531x_dev *sitdev, u8 pll_idx,
> +			       u8 input_idx)
> +{
[ ... ]
> +	/* 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);
> +}
> +
[ ... ]
> +int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
> +			    u8 input_idx)
> +{
[ ... ]
> +	for (slot = SIT9531X_PRIO_MAX_SLOTS; slot-- > 0;) {
> +		src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
> +		src_canon = sit9531x_prio_src_canon(sitdev, src);
> +		if (src_canon >= SIT9531X_PRIO_NUM_SRC)
> +			continue;
> +
> +		if (seen[src_canon] > 1) {
> +			replace = slot;
> +			break;
> +		}
> +	}
> +
> +	if (replace == SIT9531X_PRIO_MAX_SLOTS)
> +		return -ENOSPC;

[Severity: Low]
Only a slot whose canonical source is duplicated is ever chosen as the
insertion point.  Does that refuse a request when a slot names nothing
usable?

A nibble of 0xC-0xF is skipped by the src_canon >= SIT9531X_PRIO_NUM_SRC
test, and a reserved code such as 4 or 11 appearing once has seen[] == 1, so
neither is replaced and the function returns -ENOSPC even though that slot
holds no valid reference.  sit9531x_prio_table_read() returns whatever the
loaded configuration put in the nibbles, and the rest of the driver already
treats out-of-range codes as possible (SIT9531X_REF_INVALID in
sit9531x_hw_src_input()).

Should a slot whose code decodes to no valid source be preferred over
returning -ENOSPC?

[ ... ]

> @@ -1323,15 +1806,15 @@ static bool sit9531x_dpll_pin_is_registrable(struct sit9531x_dpll *sitdpll,
>  					     enum dpll_pin_direction dir,
>  					     u8 index)
>  {
> -	/*
> -	 * Only the XO pin has a complete pin-op table in this patch.
> -	 * Other pin classes are registered once their state callbacks
> -	 * land in the following patches.
> -	 */
> +	struct sit9531x_dev *sitdev = sitdpll->dev;
> +
>  	if (dir != DPLL_PIN_DIRECTION_INPUT)
>  		return false;

[Severity: Low]
The rewritten kernel-doc above says "For output pins: the pin is registrable
if this DPLL is routed to it, whether or not it is currently driving", but
the first statement of the body still rejects every non-input direction, and
sit9531x_dpll_output_pin_ops in dpll.c carries only .direction_get at this
commit.  The output branch shows up later in the series, in "dpll: sit9531x:
implement output pin state on a DPLL".

Could the output sentence be deferred to that patch, keeping the note that
other pin classes are registered once their state callbacks land?  The same
applies to the output and sync-source roles described in the commit message,
which this patch does not implement.

>  
> -	return index == SIT9531X_MAX_INPUTS;
> +	if (index == SIT9531X_MAX_INPUTS)
> +		return true;
> +
> +	return sit9531x_input_pin_is_registrable(sitdev, index);
>  }
>  
>  /*
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index b4984335e6bfc..ab9aa7db1839c 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -288,6 +288,41 @@ const struct dpll_device_ops sit9531x_dpll_device_ops = {
>   * FIXED role -- XO pin.  Always CONNECTED; it cannot be routed.
>   */
>  
[ ... ]
> +static void
> +sit9531x_dpll_selection_state_get(struct sit9531x_dev *sitdev,
> +				  const struct sit9531x_dpll *sitdpll,
> +				  u8 pin_id, enum dpll_pin_state *state)
> +{
> +	const struct sit9531x_chan *chan;
> +	bool active_input;
> +
> +	lockdep_assert_held(&sitdev->multiop_lock);
> +
> +	chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
> +	active_input = !chan->mode && chan->locked && !chan->inner_lol &&
> +		       chan->selected_ref == pin_id;
> +
> +	if (!(chan->prio_mask & BIT(sit9531x_input_hw_src(pin_id))))
> +		*state = DPLL_PIN_STATE_DISCONNECTED;
> +	else if (active_input)
> +		*state = DPLL_PIN_STATE_CONNECTED;
> +	else
> +		*state = DPLL_PIN_STATE_SELECTABLE;
> +}

[Severity: Medium]
Both tests here assume a canonicalization that the producers of the cached
state do not perform.  Can a differential pair listed by its N code report
the wrong state?

sit9531x_prio_mask_build() sets the mask from the raw nibble:

	mask |= BIT(srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK);

so for a DE pair listed as 7+pair the mask carries the N bit, while the
membership test above uses sit9531x_input_hw_src(pin_id) for the registered P
pin, and sit9531x_input_prio_present() folds N to P through
sit9531x_prio_src_canon().  The pin would read DISCONNECTED while
sit9531x_input_prio_add() (which does canonicalize) finds the source already
listed and returns 0, so a SELECTABLE request succeeds with the reported
state never following.  This half is addressed in the next patch, "dpll:
sit9531x: add support to get and set priority on input pins", where
prio_mask_build() gains src_canon = sit9531x_prio_src_canon(...).

The selected_ref half looks unaddressed through the end of the series:
sit9531x_chan_state_fetch() stores

	chan->selected_ref =
		sit9531x_hw_src_input(input_sel & SIT9531X_PRIO_NIBBLE_MASK);

which for an N-coded selection of a DE pair yields the odd lane index, and no
pin is registered for that index (sit9531x_input_pin_is_registrable() skips
it).  Does that make chan->selected_ref == pin_id unreachable for the
registered P pin, so a locked differential reference reports SELECTABLE
instead of CONNECTED?  The same comparison also gates the FFO and
phase-offset reporting later in the series.  Should selected_ref be folded
with sit9531x_prio_src_canon() at decode time?

> +
>  static int
>  sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
>  				      void *pin_priv,
> @@ -300,8 +335,144 @@ sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
>  	return 0;
>  }
>  
[ ... ]
> +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)
> +{
[ ... ]
> +	case DPLL_PIN_STATE_SELECTABLE:
> +		if (!(ref->pll_mask & pll_bit)) {
> +			rc = sit9531x_input_enable(sitdev, dpin->id);
> +			if (rc)
> +				break;
> +		}
> +		rc = sit9531x_input_prio_add(sitdev, sitdpll->id, hw_src);
> +		if (rc && !sit9531x_input_prio_present(sitdev, sitdpll->id,
> +						       hw_src)) {
> +			if (!(ref->pll_mask & pll_bit) && !ref->pll_mask)
> +				sit9531x_input_disable(sitdev, dpin->id);
> +			break;
> +		}

[Severity: Medium]
The enable and the undo are both keyed on ref->pll_mask rather than on the
receiver's actual state in ref->enabled.  Are these two cases handled?

First, when this DPLL already holds the claim but the receiver is forced off,
sit9531x_input_enable() is skipped and sit9531x_input_prio_add() finds the
source already listed and returns 0, so the request reports success while the
input stays gated and the PLL can never select it.  That combination is
reachable straight after probe: sit9531x_ref_pll_mask_fetch() seeds pll_mask
from priority-table membership only, while sit9531x_ref_state_fetch() derives
the receiver state separately with

	ref->enabled = !((force & BIT(pair)) && !(state & BIT(pair)));

Second, when this DPLL holds no claim, ref->pll_mask is empty and the
receiver was already enabled by the loaded configuration, a failing
prio_add() calls sit9531x_input_disable() and turns off an input this request
never enabled, for every DPLL.

Should the enable and the undo consult ref->enabled and only undo what this
call actually changed?

> +		/*
> +		 * 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;
[ ... ]
> +	return rc;
> +}
> +
>  static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
>  	.direction_get		= sit9531x_dpll_input_pin_direction_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,
>  };

[Severity: Low]
This is the ops table for the input pins that this patch first registers, and
it has no .prio_get/.prio_set, while sit9531x_pin_props_get() advertises

	props->dpll_props.capabilities =
			DPLL_PIN_CAPABILITIES_PRIORITY_CAN_CHANGE |
			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;

for every physical input pin.  At this commit
dpll_msg_add_pin_prio() therefore never emits DPLL_A_PIN_PRIO for those pins,
and dpll_pin_prio_set() returns -EOPNOTSUPP after passing the capability
check.  In the same vein, sit9531x_input_prio_set() added in core.c has no
caller, and sit9531x_input_prio_get() is declared in core.h without a
definition at this revision.

The following patch, "dpll: sit9531x: add support to get and set priority on
input pins", adds both callbacks and the missing definition.  Would it be
better to register the pins and advertise PRIORITY_CAN_CHANGE in the same
patch that provides the callbacks?

>  
>  /*

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260915000015.80480-1-arouhi%40sitime.com

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

* Re: [PATCH net-next v9 07/15] dpll: sit9531x: add support to get and set priority on input pins
  2026-09-15  0:00 ` [PATCH net-next v9 07/15] dpll: sit9531x: add support to get and set priority on input pins Ali Rouhi
@ 2026-09-17 15:01   ` netdev-bot+sashiko
  0 siblings, 0 replies; 33+ messages in thread
From: netdev-bot+sashiko @ 2026-09-17 15:01 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree,
	netdev, linux-kernel, kuba

Thank you for your contribution! Sashiko AI review found 7 potential
issue(s) to consider:

Critical: 0 · High: 2 · Medium: 2 · Low: 3

- [High] Self-deadlock: sit9531x_dpll_input_pin_prio_set()
  (drivers/dpll/sit9531x/dpll.c) ends with `while (changed_cnt--)…
- [High] Use-after-free / NULL dereference window:
  sit9531x_dpll_input_pin_prio_set() is the first pin op that walks the…
- [Medium] sit9531x_input_prio_get() (drivers/dpll/sit9531x/core.c)
  collapses the documented 'absent' encoding (chan->prio_last[src] == 0,…
- [Medium] Silent no-op priority set: sit9531x_input_prio_set()
  (drivers/dpll/sit9531x/core.c:671) locates only the FIRST slot holding…
- [Low] The new struct member sit9531x_chan.prio_srcs[] (core.h:152) is
  written by sit9531x_prio_mask_build() ('chan->prio_srcs[slot] = src;')…
- [Low] sit9531x_dpll_input_pin_prio_set() notifies the pin the request
  was made for, in addition to the shifted siblings: the traversal…
- [Low] sit9531x_prio_table_commit() rebuilds the newly extended cache
  (chan->prio_srcs / chan->prio_last, alongside prio_mask) from the…

--

dpll: sit9531x: add support to get and set priority on input pins

This adds .prio_get and .prio_set to the sit9531x input pin ops, backed by a
new cached view of the hardware priority table (prio_srcs[] and prio_last[])
that is refreshed whenever the table is written or polled.  Getting a
priority looks up the cached slot for the pin's source code; setting one
moves the source to the requested slot through sit9531x_input_prio_set().

A few questions below.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index 25418485e9757..484c94def578c 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -521,21 +521,81 @@ bool sit9531x_input_prio_present(struct sit9531x_dev *sitdev, u8 pll_idx,
>  }
>  
>  /*
> - * Rebuild a PLL's membership mask from the source codes of its priority
> - * table.  The mask is what the pin state getters test, so it is refreshed
> - * from exactly the values the table holds -- here after a write, and once
> - * per poll from the read-back in sit9531x_chan_state_fetch().
> + * 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)
> + *
> + * Reports the last slot this source occupied on this PLL.  The value is
> + * cached from the hardware table read at startup and refreshed after every
> + * table write and poll read-back, so pin-get reflects hardware state without
> + * issuing synchronous register reads per pin.  A source with no known slot
> + * falls back to the lowest-priority valid slot.
> + *
> + * Caller must hold sitdev->multiop_lock.
> + */
> +int sit9531x_input_prio_get(struct sit9531x_dev *sitdev, u8 pll_idx,
> +			    u8 input_idx, u8 *prio)
> +{
> +	const struct sit9531x_chan *chan;
> +	u8 slot;
> +
> +	lockdep_assert_held(&sitdev->multiop_lock);
> +
> +	if (pll_idx >= SIT9531X_NUM_PLLS)
> +		return -EINVAL;
> +	input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
> +	if (input_idx >= SIT9531X_PRIO_NUM_SRC)
> +		return -EINVAL;
> +
> +	chan = &sitdev->chan[pll_idx];
> +	slot = chan->prio_last[input_idx];
> +	if (!slot)
> +		slot = SIT9531X_PRIO_MAX_SLOTS;
> +
> +	*prio = slot - 1;
> +
> +	return 0;
> +}

[Severity: Medium]
The commit message says:

  "An input absent from the table has no priority to report, and the core is
   told so rather than handed a made-up number."

Does sit9531x_input_prio_get() actually tell the core that?  The absent
encoding documented for prio_last[] in core.h (0 = the source is not in the
table) is turned into SIT9531X_PRIO_MAX_SLOTS and the function returns 0:

	slot = chan->prio_last[input_idx];
	if (!slot)
		slot = SIT9531X_PRIO_MAX_SLOTS;

	*prio = slot - 1;

Since SIT9531X_PRIO_MAX_SLOTS is 11, *prio becomes 10, which is a valid
slot.  dpll_msg_add_pin_prio() puts whatever prio_get returns into
DPLL_A_PIN_PRIO, and dpll_msg_add_pin_dplls() calls it for every pin/dpll
reference regardless of connection state.

Can userspace then tell "input not in the priority table" apart from "input
programmed at slot 10"?

The value also does not round-trip: writing 10 back reaches
sit9531x_input_prio_set(), which returns -EINVAL when the source is not
found in the table, so
sit9531x_dpll_input_pin_prio_set() answers with "Pin is not a reference of
this DPLL; connect it first".

Would it be clearer either to return an error from prio_get for an absent
source, or to reword the changelog to match the fallback the code
implements?

> +
> +/*
> + * Refresh a PLL's cached view of its priority table from the source codes
> + * the table holds -- here after a write, and once per poll from the
> + * read-back in sit9531x_chan_state_fetch().

[Severity: Low]
sit9531x_prio_table_commit() calls sit9531x_prio_mask_build() with the
intended srcs[] array even when a slot read or write failed mid-sequence
(the "goto commit" path).  With this patch prio_last[] becomes the source of
truth for the priority reported by the new .prio_get op, so a get issued
after a failed set can report a slot the device never accepted, and after a
partial shift-write the device may hold the source in two slots and select
it at a higher priority than the driver reports.

The existing comment at the rebuild site documents this choice, and
sit9531x_chan_state_fetch() calls sit9531x_prio_table_read() plus
sit9531x_prio_mask_build() on every poll, so the window is bounded by the
poll period.  Is it worth noting in the kernel-doc for
sit9531x_input_prio_get() that the cached slot may be an intended rather
than a confirmed one until the next poll?

> + *
> + * The membership mask is what the pin state getters test; the per-slot
> + * copy and the last-slot-seen array are what priority get answers from,
> + * so neither costs a register read per pin.
>   */
>  static void sit9531x_prio_mask_build(struct sit9531x_dev *sitdev, u8 pll_idx,
>  				     const u8 *srcs)
>  {
> +	struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
> +	u8 first[SIT9531X_PRIO_NUM_SRC] = { 0 };
>  	u16 mask = 0;
> -	u8 slot;
> +	u8 slot, src, src_canon;
>  
> -	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++)
> -		mask |= BIT(srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK);
> +	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
> +		src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
> +		chan->prio_srcs[slot] = src;

[ ... ]

> diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
> index f628bd5b7a975..1ab8ebfd78da1 100644
> --- a/drivers/dpll/sit9531x/core.h
> +++ b/drivers/dpll/sit9531x/core.h
> @@ -129,6 +129,12 @@ struct sit9531x_out {
>   * @ho_freeze:		holdover freeze active
>   * @ho_valid:		holdover memory acquired, i.e. the holdover window
>   *			holds a valid estimate to fall back on
> + * @prio_srcs:		cached copy of the priority table, one source code
> + *			per slot; refreshed together with @prio_mask, so
> + *			priority reads generate no register traffic
> + * @prio_last:		slot each source occupies, plus one (0 = the source
> + *			is not in the table); refreshed from the same scan
> + *			as @prio_mask, so the two never disagree
[ ... ]
> @@ -143,6 +149,8 @@ struct sit9531x_chan {
>  	bool		inner_lol;
>  	bool		ho_freeze;
>  	bool		ho_valid;
> +	u8		prio_srcs[SIT9531X_PRIO_MAX_SLOTS];

[Severity: Low]
Is prio_srcs[] read anywhere?  Grepping drivers/dpll/sit9531x/ turns up only
this declaration and the single store in sit9531x_prio_mask_build()
("chan->prio_srcs[slot] = src;"), both at this commit and at the end of the
series.  sit9531x_input_prio_get() reads prio_last[] only.

The kernel-doc above ("so priority reads generate no register traffic") and
the comment on sit9531x_prio_mask_build() ("the per-slot copy and the
last-slot-seen array are what priority get answers from") describe a
consumer that does not exist.  Was a user of this array dropped, or can the
member and those comment lines go?

> +	u8		prio_last[SIT9531X_PRIO_NUM_SRC];
>  	u16		prio_mask;
>  };
>  
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index ab9aa7db1839c..e2ff21adfdb23 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -469,10 +469,112 @@ sit9531x_dpll_input_pin_state_on_dpll_set(const struct dpll_pin *pin,
>  	return rc;
>  }
>  
[ ... ]
> +/*
> + * 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 dpll_pin *changed[SIT9531X_MAX_INPUTS + 1];
> +	struct sit9531x_dpll_pin *sibling;
> +	struct sit9531x_dpll_pin *dpin = pin_priv;
> +	struct sit9531x_dpll *sitdpll = dpll_priv;
> +	struct sit9531x_dev *sitdev = sitdpll->dev;
> +	u8 changed_cnt = 0, hw_src, slot;
> +	int get_rc, rc;
[ ... ]
> +	mutex_lock(&sitdev->multiop_lock);
> +	rc = sit9531x_input_prio_set(sitdev, sitdpll->id,
> +				     sit9531x_input_hw_src(dpin->id),
> +				     (u8)prio);

[Severity: Medium]
This is the first caller of sit9531x_input_prio_set(); in the parent commit
the helper has none.  Can a set through it report success without changing
the effective priority?

sit9531x_input_prio_set() locates only the first slot holding the source:

	for (from = 0; from < SIT9531X_PRIO_MAX_SLOTS; from++)
		if (sit9531x_prio_src_canon(sitdev, srcs[from]) == input_idx)
			break;
	...
	srcs[prio] = input_idx;
	if (!memcmp(srcs, orig_srcs, sizeof(srcs)))
		return 0;

Duplicated source codes are the driver's own representation of an unused
slot -- sit9531x_input_prio_remove() backfills the freed tail:

	/* Backfill freed tail slots with the lowest-priority remaining src */
	while (count < SIT9531X_PRIO_MAX_SLOTS) {
		kept[count] = kept[count - 1];
		count++;
	}

So for a table like [A,B,B,B,...,B], moving B down to slot 5 shifts
duplicates onto each other, the array comes out bit-identical, the memcmp
shortcut returns 0 and no register is written.  The device keeps selecting B
at its old, higher slot, while sit9531x_prio_mask_build() records the first
occurrence, so a following get reports the old slot and not the one just
written.

The changelog says "setting it moves the input to the requested slot" --
should sit9531x_input_prio_set() clear or account for the other slots
holding the same source code?

> +	if (!rc) {
> +		list_for_each_entry(sibling, &sitdpll->pins, list) {
> +			if (!sit9531x_dpll_is_input_pin(sibling) ||
> +			    sit9531x_dpll_is_xo_pin(sibling))
> +				continue;

[Severity: High]
This is the first pin op that walks the other pins of the DPLL, and
sibling->dpll_pin is collected into changed[] without a NULL check.  Can it
race with teardown?

sit9531x_dpll_pins_unregister() in core.c does, per pin:

	list_for_each_entry_safe(pin, next, &sitdpll->pins, list) {
		sit9531x_dpll_pin_unregister(pin);
		list_del(&pin->list);
		kfree(pin);
	}

sit9531x_dpll_pin_unregister() calls dpll_pin_unregister(), which takes and
then drops dpll_lock:

drivers/dpll/dpll_core.c:dpll_pin_unregister() {
	mutex_lock(&dpll_lock);
	__dpll_pin_unregister(dpll, pin, ops, priv, NULL);
	mutex_unlock(&dpll_lock);
}

and then sets pin->dpll_pin = NULL, while list_del() and kfree() run with
dpll_lock dropped.  In that gap a netlink pin-set on a still-registered
sibling holds dpll_lock and walks &sitdpll->pins, so it can store a NULL
dpll_pin into changed[] and later hand it to dpll_pin_change_ntf() ->
__dpll_pin_change_ntf() -> dpll_pin_notify(pin, pin->clock_id, ...), or
follow a list node that is being list_del()'d and kfree()'d.

The pre-existing poll worker does a similar traversal, but
kthread_cancel_delayed_work_sync() in sit9531x_dev_stop() runs before any
unregister.  Is anything serializing this new traversal against
sit9531x_dpll_pins_unregister()?

> +
> +			hw_src = sit9531x_input_hw_src(sibling->id);
> +			get_rc = sit9531x_input_prio_get(sitdev, sitdpll->id, hw_src, &slot);
> +			if (get_rc)
> +				continue;
> +
> +			if (sibling->prio == slot)
> +				continue;
> +
> +			sibling->prio = slot;
> +			if (changed_cnt < ARRAY_SIZE(changed))
> +				changed[changed_cnt++] = sibling->dpll_pin;

[Severity: Low]
The traversal filter skips only non-input and XO pins, so dpin itself is
collected here too.  The core already emits a notification for that pin
after a successful callback:

drivers/dpll/dpll_netlink.c:dpll_pin_prio_set() {
	ret = ops->prio_set(pin, dpll_pin_on_dpll_priv(dpll, pin), dpll,
			    dpll_priv(dpll), prio, extack);
	if (ret)
		return ret;
	__dpll_pin_change_ntf(pin);
}

Does one priority change end up sending two DPLL_CMD_PIN_CHANGE_NTF
messages for the requested pin?  Skipping sibling == dpin in the loop would
leave that notification to the core.

> +		}
> +	}
> +	mutex_unlock(&sitdev->multiop_lock);
[ ... ]
> +	while (changed_cnt--)
> +		dpll_pin_change_ntf(changed[changed_cnt]);

[Severity: High]
Can this deadlock on dpll_lock?  The .prio_set callback is invoked with
dpll_lock already held:

dpll_pin_pre_doit()
	mutex_lock(&dpll_lock);
	...
dpll_pin_set_from_nlattr()
  dpll_pin_prio_set()
	ret = ops->prio_set(...);
	if (ret)
		return ret;
	__dpll_pin_change_ntf(pin);	/* lockdep_assert_held(&dpll_lock) */

and dpll_pin_change_ntf() is the wrapper that takes the same non-recursive
mutex again:

drivers/dpll/dpll_netlink.c:dpll_pin_change_ntf() {
	mutex_lock(&dpll_lock);
	ret = __dpll_pin_change_ntf(pin);
	mutex_unlock(&dpll_lock);
}

changed_cnt is non-zero in the ordinary case: sibling->prio starts at 0 from
the kzalloc_obj() in sit9531x_dpll_pin_alloc() and is never seeded, and the
loop does not exclude the pin being set, so the first "pin-set ... prio N"
with N different from the cached value queues at least one entry.

Should this use __dpll_pin_change_ntf(), the way the core does from this
context, instead of dpll_pin_change_ntf()?

>  static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
[ ... ]

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260915000015.80480-1-arouhi%40sitime.com

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

* Re: [PATCH net-next v9 08/15] dpll: sit9531x: add support to get and set frequency on pins
  2026-09-15  0:00 ` [PATCH net-next v9 08/15] dpll: sit9531x: add support to get and set frequency on pins Ali Rouhi
@ 2026-09-17 15:01   ` netdev-bot+sashiko
  0 siblings, 0 replies; 33+ messages in thread
From: netdev-bot+sashiko @ 2026-09-17 15:01 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree,
	netdev, linux-kernel, kuba

Thank you for your contribution! Sashiko AI review found 9 potential
issue(s) to consider:

Critical: 0 · High: 2 · Medium: 4 · Low: 3

- [High] sit9531x_output_divo_calc() computes the output divider with
  truncating division (divo = div64_u64(fvco, frequency)) and accepts…
- [High] sit9531x_divn_fixed() computes frac =
  mul_u64_u64_div_u64(abs(fracn), SIT9531X_DIVN_SCALE, fracd) directly…
- [Medium] sit9531x_prg_enter() unlocks the Page-3 debug block and then
  issues PRG_CMD; if the second write fails it returns the error with…
- [Medium] A phase-flush failure is reported after the new divider has
  already been committed: sit9531x_output_freq_set() returns the error…
- [Medium] When the Fvco derived from the chip's DIVN registers falls
  outside the per-PLL band assumed from the PLL index parity,…
- [Medium] The phase-flush sequence issues the PLL small-change command
  with regmap_update_bits() (sit9531x_update_pll_u8) against the PLL-…
- [Low] Output-pin frequency ops added by this patch are not reachable in
  this snapshot because sit9531x_dpll_pin_is_registrable() rejects every…
- [Low] Claim that the new frequency callbacks index sitdev->ref[] /
  sitdev->out[] with the wider DPLL pin-id namespace, reading past the…
- [Low] The new frequency code branches on and extensively documents the
  'sitime,pll-fvco' / 'sitime,output-pll-map' DT overrides (including a…

--

dpll: sit9531x: add support to get and set frequency on pins

This adds frequency_get for input and XO pins (reported from the cached
firmware description) plus frequency_get/frequency_set for output pins,
where the rate is derived from the VCO and the per-output DIVO divider.
The set path enters the output-system programming state, rewrites the
five DIVO bytes, commits, and then flushes the PLL output phase.

No Link: tags in the commit header.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index 484c94def578c..a83bf8ac83c1d 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -396,6 +396,84 @@ static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev,
>  	return 0;
>  }
>  
> +/*
> + * 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);
> +}

[Severity: Medium]
If the second write in sit9531x_prg_enter() fails, is the Page 3 debug
unlock left in place?

The only code that writes SIT9531X_DEBUG_LOCK_VAL and re-issues
SIT9531X_LOOP_LOCK is sit9531x_prg_commit(), and the caller skips it:

	rc = sit9531x_prg_enter(sitdev);
	if (rc)
		return rc;

So on a bus error for the PRG_CMD write the debug block stays writable
indefinitely, and if the failed transfer's data did reach the part the chip
is left in PRG_CMD with the output loops open, which the comment in
sit9531x_prg_commit() calls "worse than any other failure this function can
report".

Would a goto to the commit/relock path be preferable here, in the same shape
sit9531x_phase_offset_read() already uses with its "goto relock" for its own
debug window?

[ ... ]

> @@ -921,6 +1004,510 @@ int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
>  /* The output divider is a 34-bit field */
>  #define SIT9531X_DIVO_MAX			GENMASK_ULL(33, 0)
>  
> +/*

[ ... ]

> +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);

[Severity: High]
Can this division trap when the register contents are not what the driver
expects?

The numerator and denominator come straight from the chip with no
plausibility check, and the denominator is biased by one in the caller:

	*divn = sit9531x_divn_fixed(int_part, (s32)fracn_raw,
				    (u64)fracd_raw + 1);

which also makes the "if (!fracd)" guard above unreachable.

abs(fracn) can be up to 2^31, so |fracn| * 1e12 / fracd exceeds U64_MAX
whenever |fracn| / fracd is above roughly 1.8e7 -- for example DEN reading
all zeroes (fracd == 1) with a NUM above about 18,446,744, or any fracd
below about 116 with a large numerator.

On x86_64 the helper is a mulq followed by a divq:

arch/x86/include/asm/div64.h:
	/*
	 * Will generate an #DE when the result doesn't fit u64, could fix with an
	 * __ex_table[] entry when it becomes an issue.
	 */

so an out-of-range quotient is a divide-error oops rather than a value the
later "if (divn <= 0) return -ENODATA;" in sit9531x_get_fvco() could reject.
This is reachable from the new output frequency get/set as well as from
sit9531x_phase_offset_read() on an ordinary pin get.

Would rejecting fracn magnitudes at or above fracd (or clamping the
fraction) before the division be a better fit for values read off a bus?

> +
> +	return fracn < 0 ? whole - (s64)frac : whole + (s64)frac;
> +}

[ ... ]

> +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);

[Severity: Medium]
Is read-modify-write the right access for the small-change command?

SIT9531X_PLL_REG_SMALL_UPDATE is PLL-page offset 0x0F, which regs.h
describes as the per-page programming directive register (loop lock, NVM
bank, efuse program, small change update, PRG_CMD escape). The regmap has
.cache_type = REGCACHE_NONE, so sit9531x_update_pll_u8() reads the hardware
and performs no write at all when the masked bit already reads back set,
and it writes back whatever other directive bits the read returned.

If the command bit does not self-clear, does the latch here silently not
happen, and likewise the disarm latch below:

	if (!ret)
		ret = sit9531x_update_pll_u8(sitdev, pll_idx,
					     SIT9531X_PLL_REG_SMALL_UPDATE,
					     SIT9531X_SMALL_UPDATE_CMD,
					     SIT9531X_SMALL_UPDATE_CMD);

leaving the on-demand flush armed, which the comment there says the one-shot
sequence must not do?

The rest of the driver issues this command with a plain write, in
sit9531x_prio_prg_commit() and in dpll.c via sit9531x_write_pll_u8(...,
SIT9531X_PLL_REG_SMALL_UPDATE, SIT9531X_SMALL_UPDATE_CMD). Should this site
follow the same convention?

> +	if (rc)
> +		goto disarm;

[ ... ]

> +static int sit9531x_output_divo_calc(struct sit9531x_dev *sitdev, u8 out_idx,
> +				     u8 pll_idx, u64 frequency, u64 *fvco_out,
> +				     u64 *divo_out)
> +{

[ ... ]

> +	/* 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;
> +	}
> +

[ ... ]

> +	rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
> +	if (rc)
> +		return rc == -ENODATA ? -ENODEV : rc;
> +	if (!sitdev->pll_fvco[pll_idx]) {
> +		if (fvco < fvco_min)
> +			fvco = fvco_min;
> +		else if (fvco > fvco_max)
> +			fvco = fvco_max;
> +	}

[Severity: Medium]
Do the set and get paths disagree once this clamp fires?

sitdev->pll_fvco[] is zero on every board today, so any Fvco derived from
DIVN that falls outside the band picked from the PLL index parity is
replaced by the band edge, and both the divider and the cached rate come
from the fabricated value:

	sitdev->out[out_idx].freq = div64_u64(fvco, divo);

sit9531x_output_freq_get() applies no clamp:

	rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
	...
	*frequency = div64_u64(fvco, divo);

So a 6 GHz derived VCO on a low-band PLL is treated as 5.89824 GHz on set
and 6 GHz on get, and the set returns success with nothing logged. The
comment a few lines above states "A VCO that cannot be read fails the
request: programming a divider from a guessed rate would put the output far
from what was asked for while reporting success" -- is clamping to a band
edge not also programming from a guessed rate?

> +
> +	divo = div64_u64(fvco, frequency);
> +	if (!divo)
> +		return -EINVAL;

[Severity: High]
Should a request that the divider cannot represent be rejected or rounded?

This is a truncating division, and the only checks are that the divider is
non-zero and fits the 34-bit field, so the realized rate fvco/floor(fvco/f)
is always above the request with no bound on the error.

With Fvco 4.9152 GHz, a 156250000 Hz request gives DIVO 31, i.e. 158.548
MHz, about 1.5% high. A 1000000000 Hz request gives DIVO 4, i.e. 1.2288
GHz, 23% high and above the 1 GHz maximum the pin itself advertises. With
Fvco 5.89824 GHz, a 100000000 Hz request gives DIVO 58 (101.69 MHz) even
though DIVO 59 would yield 99.97 MHz, so flooring picks the worse of the two
adjacent dividers.

These values reach the driver because prop.c advertises a continuous range
for outputs that list no supported frequencies:

drivers/dpll/sit9531x/prop.c:sit9531x_pin_props_get() {
	if (dir == DPLL_PIN_DIRECTION_OUTPUT && num_freqs == 0) {
		ranges[j].min = 1;
		ranges[j].max = 1000000000ULL; /* 1 GHz */
	...
}

and dpll_pin_freq_set() accepts any value inside an advertised range.

Would rounding to the nearest achievable divider plus a representability or
tolerance check (or refusing the request) be preferable to reporting success
for a rate the part is not running?

> +
> +	/*
> +	 * DIVO is a 34-bit field.  With a band-clamped Fvco this cannot
> +	 * overflow, but a DT Fvco override is taken verbatim, so guard the
> +	 * field width rather than silently truncating the divider.
> +	 */
> +	if (divo > SIT9531X_DIVO_MAX)
> +		return -EINVAL;

[ ... ]

> +int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
> +			     u8 pll_idx, u64 frequency)
> +{

[ ... ]

> +	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 = div64_u64(fvco, divo);

[Severity: Medium]
When the flush fails, the divider has already been committed, so the part
runs at the new rate while userspace is told the set failed, and
sitdev->out[out_idx].freq still holds the old rate. Any of the six register
accesses in sit9531x_output_phase_flush() can produce that error.

Can a retry recover the skipped phase realignment?

drivers/dpll/dpll_netlink.c:dpll_pin_freq_set() {
	ret = ops->frequency_get(... &old_freq, extack);
	...
	if (freq == old_freq)
		return 0;
	...
}

The readback now returns the committed divider, so an identical retry never
re-enters frequency_set and the flush never runs -- which per the comment in
sit9531x_output_phase_flush() leaves the output with an offset that only a
power cycle used to clear.

Should the previous DIVO be restored on a flush failure, or the cache be
updated before the flush is attempted so the reported rate matches the
hardware?

> +
> +	return 0;
> +}

[ ... ]

> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index e2ff21adfdb23..88122a368053c 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -644,8 +668,78 @@ sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
>  	return 0;
>  }
>  

[ ... ]

>  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,
>  };

[Severity: Low]
This isn't a bug, but at this commit no output pin is registered:
sit9531x_dpll_pin_is_registrable() still starts with

	if (dir != DPLL_PIN_DIRECTION_INPUT)
		return false;

so these two callbacks are not dispatchable until the following patch in the
series rewrites that helper. The commit message describes the output path,
including "a frequency set on this device delays netlink traffic for every
DPLL in the system for that long", as if it were already live. Could the
message note that output pins become reachable later in the series?

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260915000015.80480-1-arouhi%40sitime.com

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

* Re: [PATCH net-next v9 10/15] dpll: sit9531x: add support to adjust output phase
  2026-09-15  0:00 ` [PATCH net-next v9 10/15] dpll: sit9531x: add support to adjust output phase Ali Rouhi
  2026-09-17  9:55   ` Ivan Vecera
@ 2026-09-17 15:01   ` netdev-bot+sashiko
  1 sibling, 0 replies; 33+ messages in thread
From: netdev-bot+sashiko @ 2026-09-17 15:01 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree,
	netdev, linux-kernel, kuba

Thank you for your contribution! Sashiko AI review found 6 potential
issue(s) to consider:

Critical: 0 · High: 0 · Medium: 5 · Low: 1

- [Medium] `struct sit9531x_out.phase_adj` is a pure software cache that
  is never seeded from the hardware.
- [Medium] A per-pin phase-adjust request ends with
  `sit9531x_output_phase_flush(sitdev, pll_idx)`, which arms…
- [Medium] `sit9531x_output_phase_adjust_set()` commits the new
  PRG_RST_DELAY bytes to the device and only afterwards updates its…
- [Medium] `sit9531x_output_freq_set()` now returns the result of the
  added phase-adjust re-encode.
- [Medium] `sit9531x_output_phase_adjust_set()` derives the output period
  from the cached `sitdev->out[out_idx].freq` and only falls back to a…
- [Low] Concern that the block comment above
  `sit9531x_output_phase_adjust_set()` in core.c documents page/base…

--

dpll: sit9531x: add support to adjust output phase

This adds phase_adjust_get/phase_adjust_set to the sit9531x output pin ops,
programming the per-output PRG_RST_DELAY block (34-bit coarse in VCO cycles
plus a 3-bit fine field in 30 ps steps) from inside the PRG_CMD programming
state, and re-encoding a cached request when the output rate changes.

A few questions about the new code below.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index bba42fe302a3b..8d857f1a0c890 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -1641,7 +1641,17 @@ int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
>  
>  	sitdev->out[out_idx].freq = div64_u64(fvco, divo);
>  
> -	return 0;
> +	/*
> +	 * The programmed reset delay counts VCO cycles against the output
> +	 * period in force when it was written, so a rate change silently
> +	 * re-times a previously requested phase adjust.  Re-encode the
> +	 * cached picosecond request against the new rate.
> +	 */
> +	if (sitdev->out[out_idx].phase_adj)
> +		rc = sit9531x_output_phase_adjust_set(sitdev, out_idx,
> +						      sitdev->out[out_idx].phase_adj);
> +
> +	return rc;
>  }

[Severity: Medium]
Can this turn a completed frequency change into a reported failure?  By the
time this runs, sit9531x_output_divo_write(), sit9531x_prg_commit() and the
phase flush have all succeeded and sitdev->out[out_idx].freq already holds
the new rate.

Any error inside sit9531x_output_phase_adjust_set() (bus error in
sit9531x_prg_enter(), the read/write loops, prg_commit or the second flush,
or -ENODEV from sit9531x_get_fvco()) becomes the return value here, with no
rollback of DIVO, and sit9531x_dpll_output_pin_frequency_set() reports it as
"Output frequency set failed".

An identical retry does not repair it either, because dpll_pin_freq_set()
asks the driver for the current rate first:

drivers/dpll/dpll_netlink.c:dpll_pin_freq_set() {
	...
	if (freq == old_freq)
		return 0;
	...
}

and sit9531x_dpll_output_pin_frequency_set()'s companion get reads DIVO back
from hardware, so the second request never reaches the driver and the phase
re-encode that failed is never performed.  For a negative cached adjust the
delay registers then stay encoded against the old period, since the
rendering T_out - |phase| depends on the period.

Would it be better to log the re-encode failure and still return success for
the frequency set?

> @@ -1731,6 +1741,206 @@ int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
>   * 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)
> +{

[ ... ]

> +	freq = sitdev->out[out_idx].freq;
> +	if (!freq) {
> +		/*
> +		 * The cache is only seeded by a DT frequency list or an
> +		 * earlier get/set; a board without supported-frequencies-hz
> +		 * would otherwise get -EINVAL on every phase request forever.
> +		 * Read the effective rate back from the divider chain.
> +		 */
> +		rc = sit9531x_output_freq_get(sitdev, out_idx, &freq);
> +		if (rc)
> +			return rc;
> +		if (!freq)
> +			return -EINVAL;
> +	}

[Severity: Medium]
The live divider is consulted only when the cache is zero.  Can a non-zero
cached value be a rate the hardware never produced?

sit9531x_pin_props_get() in prop.c seeds it from the DT list rather than
from the device:

	if (curr_freq) {
		if (dir == DPLL_PIN_DIRECTION_INPUT)
			sitdev->ref[index].freq = curr_freq;
		else
			sitdev->out[index].freq = curr_freq;
	}

where curr_freq is the first entry of supported-frequencies-hz, and the
binding only enumerates the rates the output supports.  The driver also
documents the cache as unreliable in sit9531x_output_freq_get():

	 * 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.

A wrong period lands directly in the encoding, because the negative path
computes abs_ps = t_out_ps - advance.  With a true period of 40000 ps but
6400 ps assumed (156.25 MHz listed first), a request of -1000 ps programs a
5400 ps delay instead of 39000 ps, and success is returned.  Positive
requests are mis-folded the same way once the request exceeds the assumed
period.

Nothing in dpll_pin_phase_adj_set() calls frequency_get() first, so a phase
request can be the very first operation on the pin.  Should the period be
read back from DIVO on every phase update?

[ ... ]

> +	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);

[Severity: Low]
This isn't a bug, but the block comment immediately above this function
describes the same addressing in terms of the logical output index:

 * 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).

while the code here and the new regs.h comment use the physical slot from
clkout_map[].  The code is the correct one of the two.

The wording is already corrected later in the same series, in "dpll:
sit9531x: model the inter-PLL sync net as a pair of pins", where it becomes
"Slots 0-5 live on Page 3, slots 6-11 on Page 4 ... not the logical output
index", so this only concerns this commit.

[ ... ]

> +	for (i = 0; i < ARRAY_SIZE(new_bytes); i++) {
> +		rc = sit9531x_write_u8(sitdev,
> +				       SIT9531X_REG(page, base + i),
> +				       new_bytes[i]);
> +		if (rc)
> +			goto rollback;
> +	}
> +
> +	goto commit;

[ ... ]

> +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;

[Severity: Medium]
When the five writes all succeeded and sit9531x_prg_commit() then fails,
this returns before the phase_adj assignment at the end of the function, and
nothing restores old_bytes.  The same applies to the
sit9531x_output_phase_flush() failure below.

So the delay registers of that slot hold the new coarse/fine value while
sitdev->out[out_idx].phase_adj still holds the previous one, and only a
mid-write failure takes the rollback path.

Getting back to the reported value is then not possible through the
attribute, because the core drops a request equal to what the getter
reports:

drivers/dpll/dpll_netlink.c:dpll_pin_phase_adj_set() {
	...
	if (phase_adj == old_phase_adj)
		return 0;
	...
}

The unrecorded delay is also applied at the next output-divider phase flush
of that PLL, for instance during a frequency set on a sibling output, so the
phase step appears at a point the driver never recorded.

Should these two paths roll the delay bytes back, or update the cache to
what the registers now hold?

> +	/*
> +	 * Restart the output divider phase so the freshly programmed delay is
> +	 * applied against a known edge instead of the divider's arbitrary
> +	 * running phase.
> +	 */
> +	rc = sit9531x_output_phase_flush(sitdev, pll_idx);
> +	if (rc)
> +		return rc;

[Severity: Medium]
Does a single-pin phase-adjust request disturb the sibling outputs of the
same PLL?  sit9531x_output_phase_flush() arms
SIT9531X_PLL_PHFL_ON_DEMAND_EN on the PLL page and then pulses the global
trigger:

	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1,
			       ctrl | SIT9531X_DIVO_PHASE_TRIG);

regs.h documents SIT9531X_DIVO_PHASE_TRIG as flushing the output phase with
no per-output selection, so the output-divider phase of every output fed by
that PLL looks like it gets restarted, i.e. an edge jump on the outputs that
were not adjusted.

On top of that, sit9531x_prg_enter() puts the whole output system into the
PRG_CMD state with the loops open, and sit9531x_prg_commit() re-locks them
after msleep(100), so each phase-adjust request also touches outputs of the
other PLLs.

The DPLL ABI models phase adjust as a per-pin property.  Could the scope of
this operation be narrowed, or at least described in the commit message and
in a comment?

> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index d550eb626e6de..78034b7f089cc 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -815,12 +815,68 @@ sit9531x_dpll_output_pin_state_on_dpll_set(const struct dpll_pin *pin,
>  	return rc;
>  }
>  
> +/*
> + * sit9531x_dpll_output_pin_phase_adjust_get - read output phase adjustment
> + *
> + * Returns the applied phase cached in out->phase_adj, i.e. the value
> + * sit9531x_output_phase_adjust_set() actually programmed after quantization.
> + */
> +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;
> +	struct sit9531x_dpll *sitdpll = dpll_priv;
> +	struct sit9531x_dev *sitdev = sitdpll->dev;
> +
> +	mutex_lock(&sitdev->multiop_lock);
> +	*phase_adjust = sit9531x_out_state_get(sitdev, dpin->id)->phase_adj;
> +	mutex_unlock(&sitdev->multiop_lock);
> +
> +	return 0;
> +}

[Severity: Medium]
Is the value returned here ever reconciled with the device?  phase_adj is
zeroed by the devm_kzalloc() in sit9531x_i2c_probe(),
sit9531x_out_state_fetch() reads output mode, forced Hi-Z and routing but
never the five PROG6..PROG2 delay bytes, and the only assignment is at the
tail of sit9531x_output_phase_adjust_set().

Since that setter commits with SIT9531X_UPDATE_NVM through
sit9531x_prg_commit(), and the delay fields are part of the profile the chip
loads before probe, the device can hold a non-zero delay while this reports
0 - after a module unload/reload, after a warm reboot, or on a board whose
profile programmed a delay.

A request of 0 ps meant to clear such a delay then never reaches the driver:

drivers/dpll/dpll_netlink.c:dpll_pin_phase_adj_set() {
	...
	if (phase_adj == old_phase_adj)
		return 0;
	...
}

and the re-encode added to sit9531x_output_freq_set() is gated on
if (sitdev->out[out_idx].phase_adj), so an unrecorded hardware delay is not
re-timed on a rate change either.

This also does not match the commit message:

    the two and what the core reads back is what the registers hold rather
    than what was asked for

nor the new kernel-doc "phase adjust the delay registers actually realize",
since the registers are never read for this purpose.  This looks unchanged
at the end of the series.

Could phase_adjust_get() decode the PRG_RST_DELAY block, or the startup
state fetch seed phase_adj from it?

[ ... ]

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260915000015.80480-1-arouhi%40sitime.com

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

* Re: [PATCH net-next v9 09/15] dpll: sit9531x: implement output pin state on a DPLL
  2026-09-15  0:00 ` [PATCH net-next v9 09/15] dpll: sit9531x: implement output pin state on a DPLL Ali Rouhi
@ 2026-09-17 15:01   ` netdev-bot+sashiko
  0 siblings, 0 replies; 33+ messages in thread
From: netdev-bot+sashiko @ 2026-09-17 15:01 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree,
	netdev, linux-kernel, kuba

Thank you for your contribution! Sashiko AI review found 5 potential
issue(s) to consider:

Critical: 0 · High: 0 · Medium: 4 · Low: 1

- [Medium] The patch silently turns on DPLL registration of output pins.
- [Medium] sit9531x_dpll_output_pin_state_on_dpll_get()
  (drivers/dpll/sit9531x/dpll.c:763-772) discards the return value of…
- [Medium] sit9531x_output_hiz_write() (core.c:527-554) performs four
  independent read-modify-write register operations for a mute (DIFF…
- [Medium] The Hi-Z force/value register pair is written in a fail-unsafe
  order.
- [Low] Concern (validated as resolved later in this series, therefore
  discarded by the baseline decision): at this commit, registering…

--

dpll: sit9531x: implement output pin state on a DPLL

This adds state_on_dpll_get/set for sit9531x output pins, reporting an
output as connected while it drives and disconnected while it is muted.
Muting is done by forcing the pad to Hi-Z through the per-slot force/state
register pairs, with a cached mute state that is marked stale when the
post-write read-back fails.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index a83bf8ac83c1d..bba42fe302a3b 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -396,6 +396,47 @@ static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev,
>  	return 0;
>  }
>  
> +/*
> + * sit9531x_output_state_refresh - read an output's mute state back
> + *
> + * Used when a mute could not be confirmed at the time it was written.  The
> + * driver does not poll output state, so without this the cached value would
> + * stand until something else happened to write it.
> + *
> + * Caller must hold sitdev->multiop_lock.
> + */
> +int sit9531x_output_state_refresh(struct sit9531x_dev *sitdev, u8 out_idx)
> +{
> +	bool muted;
> +	int rc;
> +
> +	lockdep_assert_held(&sitdev->multiop_lock);
> +
> +	rc = sit9531x_output_forced_hiz(sitdev, out_idx, &muted);
> +	if (rc)
> +		return rc;
> +
> +	sitdev->out[out_idx].enabled = !muted;
> +	sitdev->out[out_idx].state_stale = false;
> +
> +	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);
> +}
> +

[ ... ]

> @@ -474,6 +515,166 @@ static int sit9531x_prg_commit(struct sit9531x_dev *sitdev)
>  	return rc2 ? rc2 : rc3;
>  }
>  
> +/*
> + * sit9531x_output_hiz_write - mute or unmute an output
> + *
> + * Muting takes control of the pin (MASK=1) and drives it low (STATE=0) on
> + * both the differential and the single-ended register pair, because the
> + * output must go quiet whichever way it is wired; unmuting hands it back
> + * to the device's own state machine.  The caller must already be in the
> + * programming state.
> + */
> +static int sit9531x_output_hiz_write(struct sit9531x_dev *sitdev, u8 slot,
> +				     bool mute)
> +{
> +	struct sit9531x_hiz_regs r;
> +	int rc;
> +
> +	sit9531x_output_get_hiz_regs(slot, &r);
> +
> +	if (!mute) {
> +		rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, false);
> +		if (rc)
> +			return rc;
> +
> +		return sit9531x_hiz_set_bit(sitdev, r.se_mask, r.bit, false);
> +	}
> +
> +	rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, true);
> +	if (rc)
> +		return rc;
> +	rc = sit9531x_hiz_set_bit(sitdev, r.diff_state, r.bit, false);
> +	if (rc)
> +		return rc;
> +	rc = sit9531x_hiz_set_bit(sitdev, r.se_mask, r.bit, true);
> +	if (rc)
> +		return rc;
> +
> +	return sit9531x_hiz_set_bit(sitdev, r.se_state, r.bit, false);
> +}

[Severity: Medium]
Should sit9531x_output_hiz_write() unwind the bits it already wrote when
one of the four register operations fails?

Each of the four steps is an independent read-modify-write, and the
function returns on the first error, so a failure after step one leaves
r.diff_mask with the override-enable bit set while r.diff_state was never
programmed, and a failure after step three leaves r.se_mask set with
r.se_state untouched.

The caller then commits that half-programmed pair anyway:

	rc = sit9531x_output_hiz_write(sitdev, slot, true);
	...
	ret = sit9531x_prg_commit(sitdev);
	if (ret && !rc)
		rc = ret;

Nothing afterwards clears the MASK bit that was set, so a mute request can
leave the pad's override armed with a forced value that comes from the
loaded configuration blob rather than from this request.

The residue is also invisible to the driver's own read-back, since
sit9531x_output_forced_hiz() only inspects the pair matching out[].cmos:

	if (sitdev->out[out_idx].cmos) {
		mask_reg = r.se_mask;
		state_reg = r.se_state;
	} else {
		mask_reg = r.diff_mask;
		state_reg = r.diff_state;
	}

so a differential output whose SE writes failed reads back as cleanly muted
while a stale SE override stays armed on the same slot.

sit9531x_output_divo_write() in this same file already restores the bytes it
managed to write on its rollback: path before committing.  Would the same
approach fit here?

[Severity: Medium]
Is the write order of the pair fail-safe?  MASK (the override enable) is
asserted before STATE (the forced value) on both pairs:

	rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, true);
	if (rc)
		return rc;
	rc = sit9531x_hiz_set_bit(sitdev, r.diff_state, r.bit, false);

By the driver's own decoding in sit9531x_output_forced_hiz(), muted means
MASK set and STATE clear, so the intermediate MASK=1/STATE=1 combination is
a driver-asserted override that pins the pad driven.  The previous STATE
value comes from the initial-config blob, and the un-mute path clears only
MASK without normalising STATE:

	if (!mute) {
		rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, false);
		if (rc)
			return rc;

		return sit9531x_hiz_set_bit(sitdev, r.se_mask, r.bit, false);
	}

so STATE=1 at the moment MASK is set looks possible.

In the success case the pad then sits under a wrong-valued override for one
I2C transaction; if the STATE write fails, sit9531x_prg_commit() still runs
and MASK=1/STATE=1 becomes the committed configuration, i.e. the pad is
forced driving as the outcome of a mute request, and
sit9531x_output_forced_hiz() reports it as not muted.

Would programming STATE first and MASK second (STATE being inert while MASK
is clear) avoid the intermediate and the persistent case?

> +
> +/*
> + * sit9531x_output_disable - mute an output (force Hi-Z)
> + * @index:	logical output index (0..info->num_outputs-1)
> + *
> + * Sets MASK and clears 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;
> +	bool muted;
> +	u8 slot;
> +	int rc, ret, state_rc;
> +
> +	lockdep_assert_held(&sitdev->multiop_lock);
> +
> +	if (index >= info->num_outputs)
> +		return -EINVAL;
> +
> +	slot = info->clkout_map[index];
> +	rc = sit9531x_prg_enter(sitdev);
> +	if (rc)
> +		return rc;
> +
> +	rc = sit9531x_output_hiz_write(sitdev, slot, true);
> +
> +	/*
> +	 * 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;
> +
> +	/*
> +	 * Keep the software state aligned to what hardware now drives even
> +	 * when one write in the sequence failed. The commit above may have
> +	 * applied a partial mask/state combination.
> +	 */
> +	state_rc = sit9531x_output_forced_hiz(sitdev, index, &muted);
> +	if (!state_rc) {
> +		sitdev->out[index].enabled = !muted;
> +		sitdev->out[index].state_stale = false;
> +	} else {
> +		/*
> +		 * The writes may well have landed; what failed is the proof.
> +		 * Mark the cached state for a read-through rather than
> +		 * reporting the value it had before this call.
> +		 */
> +		sitdev->out[index].state_stale = true;
> +		if (!rc)
> +			rc = state_rc;
> +	}
> +
> +	return rc;
> +}
> +

[ ... sit9531x_output_enable() snipped, same structure ... ]

> @@ -2455,13 +2658,23 @@ static bool sit9531x_dpll_pin_is_registrable(struct sit9531x_dpll *sitdpll,
>  {
>  	struct sit9531x_dev *sitdev = sitdpll->dev;
>  
> -	if (dir != DPLL_PIN_DIRECTION_INPUT)
> +	if (dir == DPLL_PIN_DIRECTION_INPUT) {
> +		if (index == SIT9531X_MAX_INPUTS)
> +			return true;
> +		if (index == SIT9531X_INTSYNC_PIN_ID)
> +			return false;
> +
> +		return sit9531x_input_pin_is_registrable(sitdev, index);
> +	}
> +
> +	if (index == SIT9531X_INTSYNC_OUT_PIN_ID)
>  		return false;
>  
> -	if (index == SIT9531X_MAX_INPUTS)
> -		return true;
> +	if (index >= sitdev->info->num_outputs)
> +		return false;
>  
> -	return sit9531x_input_pin_is_registrable(sitdev, index);
> +	return sitdev->out[index].pll_idx == sitdpll->id &&
> +	       sitdev->out[index].routed;
>  }

[Severity: Medium]
Could the commit message mention that this hunk starts registering output
pins with the DPLL core?

Before this change sit9531x_dpll_pin_is_registrable() returned false for
every non-input direction, so sit9531x_dpll_pins_register() skipped all
output ids:

		if (!sit9531x_dpll_pin_is_registrable(sitdpll, dir, id))
			continue;

and no output pin was ever passed to dpll_pin_register().  After it, every
routed output whose pll_idx matches the DPLL is registered, which makes the
whole sit9531x_dpll_output_pin_ops table reachable for the first time,
including .frequency_set added by the earlier "add support to get and set
frequency on pins" patch, plus the output pin properties built by
sit9531x_pin_props_get().

The changelog describes only the new state_on_dpll_get/set, so the fact
that output pins begin to appear in pin dumps and become settable from
userspace is not documented.

As an aside, the earlier "register DPLL devices and pins" commit already
claimed to register "the outputs the loaded configuration routes to a PLL",
which its code did not do; this hunk is what implements it.

> diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
> index 1ab8ebfd78da1..db9c73a79b041 100644
> --- a/drivers/dpll/sit9531x/core.h
> +++ b/drivers/dpll/sit9531x/core.h
> @@ -100,6 +100,8 @@ struct sit9531x_ref {
>   * @enabled:		output is driving, i.e. not forced into Hi-Z
>   * @cmos:		output is wired single-ended; the Hi-Z pair that
>   *			speaks for it is the SE one, not the differential
> + * @state_stale:	the cached mute state could not be confirmed against
> + *			hardware and has to be read back before it is reported
>   * @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)

[ ... ]

> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index 88122a368053c..d550eb626e6de 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -736,10 +736,91 @@ sit9531x_dpll_output_pin_frequency_set(const struct dpll_pin *pin,
>  	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;
> +	struct sit9531x_dev *sitdev = sitdpll->dev;
> +	const struct sit9531x_out *out;
> +
> +	/*
> +	 * A mute whose read-back failed left the cache unconfirmed; there is
> +	 * no poll of output state to correct it, so read it here rather than
> +	 * report a value that may predate the request.
> +	 */
> +	if (sitdev->out[dpin->id].state_stale) {
> +		mutex_lock(&sitdev->multiop_lock);
> +		sit9531x_output_state_refresh(sitdev, dpin->id);
> +		mutex_unlock(&sitdev->multiop_lock);
> +	}
> +
> +	out = sit9531x_out_state_get(sitdev, dpin->id);
> +	*state = out->enabled ? DPLL_PIN_STATE_CONNECTED
> +			      : DPLL_PIN_STATE_DISCONNECTED;
> +	return 0;
> +}

[Severity: Medium]
Should the return value of sit9531x_output_state_refresh() be propagated
here?

When the read-back fails, sit9531x_output_state_refresh() returns the error
from sit9531x_output_forced_hiz() without touching out[].enabled and
without clearing out[].state_stale:

	rc = sit9531x_output_forced_hiz(sitdev, out_idx, &muted);
	if (rc)
		return rc;

	sitdev->out[out_idx].enabled = !muted;
	sitdev->out[out_idx].state_stale = false;

The getter drops that error and still emits the cached value with return 0,
so a pin whose drive state is unknown is reported to userspace as an
authoritative connected/disconnected with no extack.

The sequence would be: pin_set state=DISCONNECTED reaches
sit9531x_output_disable(), its post-commit read fails and sets
out[].state_stale = true while out[].enabled still holds the pre-request
value; the next pin_get sees state_stale, the refresh fails again on the
same broken bus, and the pre-request value is reported as success.

That also contradicts the field's own kernel-doc, which says the cached
mute state "has to be read back before it is reported", and the comment
above the call, which says it reads it "rather than report a value that may
predate the request".

Other getters in this driver do propagate such failures:
sit9531x_dpll_output_pin_frequency_get() returns rc, and
sit9531x_dpll_input_pin_phase_offset_get() sets an extack and returns rc.
This is still the case at the end of the series.

[ ... sit9531x_dpll_output_pin_state_on_dpll_set() snipped ... ]

>  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,
>  };

[Severity: Low]
This one is resolved later in the same series, but worth noting for
bisectability: at this commit the newly registered output pins advertise a
phase-adjust window while no callback implements it.

The output branch of sit9531x_pin_props_get() in prop.c publishes:

		props->dpll_props.phase_range.min = -1000000000; /* -1 ms in ps */
		props->dpll_props.phase_range.max =  1000000000; /* +1 ms in ps */
		...
		props->dpll_props.phase_gran = 1;

while sit9531x_dpll_output_pin_ops above has neither .phase_adjust_get nor
.phase_adjust_set, so a pin_set(phase-adjust) inside the advertised range
answers -EOPNOTSUPP.  core.h also declares sit9531x_output_phase_adjust_set()
with no definition in the tree at this point.

The next patch, "dpll: sit9531x: add support to adjust output phase", adds
both callbacks and defines that function, so the gap only exists between
these two commits.  Would reordering, or folding, keep each commit
self-consistent?

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260915000015.80480-1-arouhi%40sitime.com

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

* Re: [PATCH net-next v9 12/15] dpll: sit9531x: add support to get phase offset on the connected input pin
  2026-09-15  0:00 ` [PATCH net-next v9 12/15] dpll: sit9531x: add support to get phase offset on the connected input pin Ali Rouhi
@ 2026-09-17 15:01   ` netdev-bot+sashiko
  0 siblings, 0 replies; 33+ messages in thread
From: netdev-bot+sashiko @ 2026-09-17 15:01 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree,
	netdev, linux-kernel, kuba

Thank you for your contribution! Sashiko AI review found 10 potential
issue(s) to consider:

Critical: 0 · High: 0 (1) · Medium: 4 · Low: 6

- [Medium] sit9531x_dpll_input_pin_phase_offset_get() publishes a TDC
  reading as DPLL_A_PIN_PHASE_OFFSET without checking the hardware…
- [Medium] In-band error overloading defeats two documented contracts.
- [Medium] The 35-bit TDC magnitude assembled from five raw register
  reads is used unvalidated as the numerator of…
- [Medium] sit9531x_dpll_input_pin_phase_offset_get() decides which pin a
  TDC sample belongs to from the driver's cached channel state, but the…
- [Low] The new comment block in
  sit9531x_dpll_input_pin_phase_offset_get() claims 'the open-loop…
- [Low] Dead and internally inconsistent debug-window register
  definitions in regs.h.
- [Low] Dead store into cached state: every path of
  sit9531x_dpll_input_pin_phase_offset_get() writes the measured value…
- [Low] Duplicate macros for the same register offset and the same magic
  key values.
- [Low] The new comment in sit9531x_dpll_input_pin_phase_offset_get()
  states 'The framework expects successive values to be averaged, which…
- [Low] sit9531x_phase_offset_read() programs two persistent pieces of
  PLL-page debug state but restores neither: it sets…

Pre-existing issues:
- [High] Recursive acquisition of the global dpll_lock:
  sit9531x_dpll_input_pin_prio_set() calls dpll_pin_change_ntf() for the…

--

dpll: sit9531x: add support to get phase offset on the connected input pin

This adds a .phase_offset_get callback for sit9531x input pins, backed by
a new core helper that unlocks the PLL page debug window, points the
readback mux at the on-chip time-to-digital converter and assembles a
35-bit sample plus sign into picoseconds using Fvco.  Non-selected pins
and PLLs without a programmed divider report zero.

A few questions below.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index 46179a9e13c32..356d1eac4ec54 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -2132,6 +2132,154 @@ int sit9531x_output_pulse_ctrl_set(struct sit9531x_dev *sitdev,
>  	return rc;
>  }
>  
> +/*
> + * sit9531x_phase_offset_read - read phase difference via TDC
> + * @phase_ps:	output phase difference in picoseconds
> + *
> + * Reads the Time-to-Digital Converter (TDC) signed 35-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, mag_ps;
> +	s64 tdc_signed;
> +	u64 tdc_raw;
> +	int rc, lock_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)
> +		goto relock;
> +
> +	/*
> +	 * 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)
> +		goto relock;
> +	rc = sit9531x_write_pll_u8(sitdev, pll_idx,
> +				   SIT9531X_PLL_REG_DBG_READ_CODE,
> +				   SIT9531X_DBG_READ_CODE_TDC);
> +	if (rc)
> +		goto relock;

[Severity: Low]
Should the previous contents of Dig_Sys_WriteCode (PLL 0xB4) and
Dig_Sys_ReadCode (PLL 0xB3) be saved and restored here?

sit9531x_phase_offset_read() sets SIT9531X_DBG_LOW_FREQ_CLK_BIT and
overwrites the read code with the TDC tap, but the relock epilogue only
rewrites the key register:

	lock_rc = sit9531x_write_pll_u8(sitdev, pll_idx,
					SIT9531X_PLL_REG_DEBUG,
					SIT9531X_PLL_DEBUG_LOCK);

So after the first pin-get on a connected input the PLL is left with a
debug sampling clock selected and the readback mux parked on read code
69, with the key re-locked so nothing can observe it.

sit9531x_output_phase_flush() takes the opposite approach for
PHFL_ON_DEMAND_EN, with the comment that leaving it set would be
"exactly the persistent side effect the one-shot sequence must not
have".

The later series commit "dpll: sit9531x: add support to get fractional
frequency offset" states the same hardware property ("The device never
clears the bit, so a read that left it to whatever the previous one set
would depend on the order the taps happened to be read in") and works
around it in sit9531x_dbg_sample(), but sit9531x_phase_offset_read()
itself is unchanged at the end of the series.

> +
> +	/*
> +	 * Latch a sample by reading the trigger register.  A single
> +	 * read returns the previous latch, so read it three times as
> +	 * the documented phase-difference procedure does.
> +	 */
> +	for (i = 0; i < SIT9531X_DBG_LATCH_READS; i++) {
> +		rc = sit9531x_read_pll_u8(sitdev, pll_idx,
> +					  SIT9531X_PLL_REG_DBG_TRIGGER, &v);
> +		if (rc)
> +			goto relock;
> +	}
> +
> +	tdc_raw = 0;
> +
> +	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
> +				  SIT9531X_PLL_REG_DBG_DATA_4, &v);
> +	if (rc)
> +		goto relock;
> +	sign = !!(v & BIT(SIT9531X_TDC_SIGN_BIT));
> +	tdc_raw = (u64)(v & SIT9531X_TDC_MAG_HI_MASK) << 32;
> +

[ ... reads of DBG_DATA_3 .. DBG_DATA_0 snipped ... ]

> +	/*
> +	 * Apply sign.  Per the register map 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.  -ENODATA means DIVN is not
> +	 * programmed (PLL unused on this board) -- skip silently rather
> +	 * than spamming the log on every poll cycle.  A register access
> +	 * failure is returned as an error, not folded into a fabricated
> +	 * 0 ps measurement.
> +	 */
> +	rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
> +	if (rc == -ENODATA) {
> +		dev_dbg(sitdev->dev, "PLL%c: Fvco unknown, skip TDC\n",
> +			'A' + pll_idx);
> +		rc = -ENODEV;
> +		goto relock;
> +	}
> +	if (rc)
> +		goto relock;

[Severity: Medium]
Can a bus error reach userspace as a valid 0 ps measurement here?

The private "no data" condition is re-encoded as -ENODEV, and the
netlink caller then treats any -ENODEV as "no programmed DIVN":

drivers/dpll/sit9531x/dpll.c:sit9531x_dpll_input_pin_phase_offset_get() {
	if (rc == -ENODEV) {
		dpin->phase_offset = 0;
		*phase_offset = 0;
		return 0;
	}
}

-ENODEV is also an errno the regmap/I2C layer produces (for instance a
USB-attached I2C adapter that has been unplugged), and
sit9531x_read_u8()/sit9531x_write_u8() pass regmap's errno through
verbatim.  A register access failure inside this function, or inside
sit9531x_get_fvco(), would then be reported as a successful zero
reading with nothing logged.

That seems to contradict both the comment just above ("A register access
failure is returned as an error, not folded into a fabricated 0 ps
measurement") and sit9531x_get_fvco()'s own kernel-doc ("A bus failure
is never folded into the -ENODATA case, so callers can fail a request
instead of acting on a guessed rate").

Would propagating -ENODATA to the caller, and matching on that instead,
keep those two contracts intact?

> +
> +	/*
> +	 * phase_diff (seconds) = tdc_code / fvco
> +	 * phase_diff (ps) = tdc_code * 1e12 / fvco
> +	 *
> +	 * mul_u64_u64_div_u64() keeps the exact Hz denominator; dividing
> +	 * by whole MHz instead would lose up to ~40 ppm of scale on a
> +	 * fractional-DIVN Fvco.
> +	 */
> +	mag_ps = mul_u64_u64_div_u64(tdc_signed < 0 ? -tdc_signed : tdc_signed,
> +				     1000000000000ULL, fvco);
> +	*phase_ps = tdc_signed < 0 ? -(s64)mag_ps : (s64)mag_ps;

[Severity: Medium]
Does this division need a lower bound on fvco?

The numerator comes straight from five register reads and can be up to
0x7FFFFFFFF (about 3.44e10).  sit9531x_get_fvco() only rejects
divn <= 0 and a product that truncates to zero, so a garbage or
partially programmed DIVN (int_part 0 with a small fracn over a large
fracd) can return an fvco of a few Hz.  With a full-scale code the u64
quotient overflows once fvco drops below roughly 1.9 kHz, and the x86
helper traps in that case:

arch/x86/include/asm/div64.h:mul_u64_add_u64_div_u64() {
	/*
	 * Will generate an #DE when the result doesn't fit u64, could fix with an
	 * __ex_table[] entry when it becomes an issue.
	 */
}

The sibling helper guards against exactly this class of input:

drivers/dpll/sit9531x/core.c:sit9531x_pll_ffo_ppt() {
	if (configured < SIT9531X_DIVN_SCALE)
		return -ENODATA;
}

There is a second overflow further along the path.  In
sit9531x_dpll_input_pin_phase_offset_get():

	offset *= DPLL_PHASE_OFFSET_DIVIDER;

This is unchecked, so mag_ps above roughly 9.2e15 ps (reachable with an
fvco of a few MHz and a large code) overflows s64 before the value is
put in the netlink attribute.

> +
> +	rc = 0;
> +
> +relock:
> +	/*
> +	 * Close the debug window again.  The key register opens every debug
> +	 * register on this PLL while it holds the unlock value, and this read
> +	 * runs on every pin-get of a connected input, so leaving it open
> +	 * would mean normal monitoring permanently unlocks the block.
> +	 */
> +	lock_rc = sit9531x_write_pll_u8(sitdev, pll_idx,
> +					SIT9531X_PLL_REG_DEBUG,
> +					SIT9531X_PLL_DEBUG_LOCK);
> +	if (lock_rc && !rc)
> +		rc = lock_rc;
> +
> +	return rc;
> +}
> +
>  /*
>   * sit9531x_ref_state_fetch - read input reference status from hardware
>   * @index:	logical input index
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index 0cad081eb5995..1dfd83b635e23 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -603,6 +603,107 @@ sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
>  	return 0;
>  }
>  
> +/*
> + * sit9531x_dpll_input_pin_phase_offset_get - phase offset of a reference
> + *
> + * What this reports, and what it deliberately does not:
> + *
> + * The ABI defines the attribute as the phase difference between the signal
> + * on a pin and its parent DPLL device, so this is the loop's own residual
> + * error, sampled with the loop closed.  On a locked DPLL it therefore
> + * trends small -- that is the measurement, not an artefact of it.  The
> + * framework expects successive values to be averaged, which suits a
> + * closed-loop residual and not a one-shot open-loop capture.

[Severity: Low]
This isn't a bug, but does the framework actually average anything?

drivers/dpll/dpll_netlink.c:dpll_msg_add_phase_offset() calls
ops->phase_offset_get() once and puts the returned value straight into
the message; it keeps no history:

	ret = ops->phase_offset_get(pin, dpll_pin_on_dpll_priv(dpll, pin),
				    dpll, dpll_priv(dpll), &phase_offset,
				    extack);
	if (ret)
		return ret;
	if (nla_put_64bit(msg, DPLL_A_PIN_PHASE_OFFSET, ...))

Documentation/driver-api/dpll.rst says the reported value "may be
computed as the average of prior values and the current measurement",
tied to the device-configured DPLL_A_PHASE_OFFSET_AVG_FACTOR attribute,
which this driver does not implement.  Returning raw samples is fine,
but the comment states an expectation the core does not have.

> + *
> + * The chip can also measure the reference against the local oscillator
> + * with the outer loop's correction frozen, which is a different quantity
> + * and the one the documented phase-difference procedure produces.  That
> + * needs the digital loop filter held (and, on the 1PPS PLL, the automatic
> + * phase- and frequency-lock helpers held off), which leaves the PLL
> + * undisciplined until it is released.  A netlink read must not do that,
> + * so the open-loop measurement lives in a debugfs helper that owns the
> + * freeze and restores it; it is not this callback.

[Severity: Low]
Which debugfs helper does this refer to?  A grep for "debugfs" under
drivers/dpll/sit9531x/ at this commit returns only this comment line,
and the same is true at the end of the series: there is no debugfs
registration, no file operations and no loop-filter freeze/restore code
in the driver.  Should this paragraph be dropped or reworded until such
a helper exists?

> + *
> + * Precondition, which this callback cannot create: the TDC compares
> + * against a signal the PLL drives, so a PLL driving no output with its
> + * zero-delay buffer off has nothing to measure.  SiTime confirms this is
> + * a property of the hardware rather than of their measurement script.
> + * The script satisfies it by mapping a spare output and restarting the
> + * PLL -- side effects that do not belong in a getter, so a reading taken
> + * in that state is simply not meaningful.

[Severity: Medium]
If a reading taken in that state is not meaningful, should the callback
refuse to publish one?

sit9531x_dpll_input_pin_phase_offset_get() gates only on the cached
DPLL_PIN_STATE_CONNECTED selection state and on the -ENODEV (no DIVN)
case.  Nothing checks that the PLL drives an enabled output, and
nothing reads SIT9531X_PLL_ZDB_EN_BIT -- that macro is defined in
regs.h but referenced nowhere in core.c or dpll.c, here or at the end
of the series.

So a locked PLL with a programmed DIVN, all outputs muted and the
zero-delay buffer off returns rc == 0 and whatever the debug data
registers happen to hold is emitted as DPLL_A_PIN_PHASE_OFFSET.  The
attribute carries no validity marker, so userspace cannot tell that
value from a real measurement.

The commit message describes only the connected/non-connected
distinction ("Only the input a PLL has actually selected has a phase
offset against it") and does not mention this precondition.

> + *
> + * Non-selected pins and a PLL with no programmed divider report zero
> + * rather than an error: the DPLL core propagates any error from this
> + * callback and fails the whole pin dump with it, unlike the frequency
> + * offset getter, where -ENODATA makes the core omit the attribute.  There
> + * is no per-pin "no data" for phase offset, so it is a value or no
> + * callback at all.
> + */

[ ... function prologue snipped ... ]

> +	/*
> +	 * 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.
> +	 */
> +	sit9531x_dpll_selection_state_get(sitdev, sitdpll, dpin->id, &state);
> +	if (state != DPLL_PIN_STATE_CONNECTED) {
> +		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);

[Severity: Medium]
Can this attribute a live TDC sample to the wrong pin?

sit9531x_dpll_selection_state_get() is a pure cache reader:

	chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
	active_input = !chan->mode && chan->locked && !chan->inner_lol &&
		       chan->selected_ref == pin_id;

chan->selected_ref is written only by the monitor path, in
sit9531x_chan_state_fetch():

	chan->selected_ref =
		sit9531x_hw_src_input(input_sel & SIT9531X_PRIO_NIBBLE_MASK);

and that monitor requeues itself twice a second:

	/* Run twice a second */
	kthread_queue_delayed_work(sitdev->kworker, &sitdev->work,
				   msecs_to_jiffies(SIT9531X_STATUS_POLL_MS));

multiop_lock serialises the driver's register accesses, but it does not
stop the device from re-selecting a reference on its own (priority
selection, input failure, revert).  So:

  monitor caches selected_ref = IN0
  hardware fails over to IN1
  userspace issues DPLL_CMD_PIN_GET within the next ~500 ms

For IN0 the cache still says CONNECTED, and the TDC sample (the VCO
versus IN1 residual) is published as IN0's phase offset, while IN1
reports 0.

Would reading the live selected-reference register under multiop_lock
next to the TDC sample, and publishing only if it still names this pin,
close that window?

> +
> +	/*
> +	 * -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;
> +	}
> +

[ ... unit-scaling comment snipped ... ]

> +	offset *= DPLL_PHASE_OFFSET_DIVIDER;
> +
> +	dpin->phase_offset = offset;
> +	*phase_offset = offset;
> +	return 0;
> +}
> +

[Severity: Low]
Is dpin->phase_offset read anywhere?

All three paths of sit9531x_dpll_input_pin_phase_offset_get() store into
struct sit9531x_dpll_pin::phase_offset, documented in dpll.h as
"in 1/DPLL_PHASE_OFFSET_DIVIDER picosecond units", but a grep for
phase_offset under drivers/dpll/sit9531x/ finds only these assignments
plus the member declaration, at this commit and at the end of the
series.

The per-DPLL monitor does compare cached pin state and notify:

drivers/dpll/sit9531x/dpll.c:sit9531x_dpll_changes_check() {
			pin->pin_state = state;
			dpll_pin_change_ntf(pin->dpll_pin);
}

but it never samples or compares the phase offset, so there is no
phase-offset change notification and the cached field has no consumer.

>  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,
> @@ -610,6 +711,7 @@ static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
>  	.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,
                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

[Severity: High]
This is a pre-existing issue and was not introduced by this patch --
it comes from the earlier series patch "dpll: sit9531x: add support to
get and set priority on input pins" -- but can the prio_set callback
reached through this table deadlock on dpll_lock?

DPLL_CMD_PIN_SET is registered with a locking pre_doit:

drivers/dpll/dpll_nl.c {
		.cmd		= DPLL_CMD_PIN_SET,
		.pre_doit	= dpll_pin_pre_doit,
}

drivers/dpll/dpll_netlink.c:dpll_pin_pre_doit() {
	mutex_lock(&dpll_lock);
	if (GENL_REQ_ATTR_CHECK(info, DPLL_A_PIN_ID)) {
}

and the callback then calls the locking notification helper for every
sibling whose slot changed:

drivers/dpll/sit9531x/dpll.c:sit9531x_dpll_input_pin_prio_set() {
	while (changed_cnt--)
		dpll_pin_change_ntf(changed[changed_cnt]);
}

drivers/dpll/dpll_netlink.c:dpll_pin_change_ntf() {
	mutex_lock(&dpll_lock);
	ret = __dpll_pin_change_ntf(pin);
	mutex_unlock(&dpll_lock);
}

That re-acquires a mutex the same task already holds, on essentially
every successful priority set.  The core provides
__dpll_pin_change_ntf() for callers already under dpll_lock, and
zl3073x uses that variant from its pin callbacks.  Would switching to
__dpll_pin_change_ntf() here be correct?  The current code is still
present at the end of the series.

>  	.phase_offset_get	= sit9531x_dpll_input_pin_phase_offset_get,
>  };
>  
>  /*
> diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
> index 9d515e109feb5..08eae8a2c279a 100644
> --- a/drivers/dpll/sit9531x/regs.h
> +++ b/drivers/dpll/sit9531x/regs.h
> @@ -271,6 +271,44 @@
>  #define SIT9531X_PLL_REG_DIVN_NUM		0x32  /* 4 bytes (0x32-0x35) */
>  #define SIT9531X_PLL_REG_DIVN_DEN		0x38  /* 4 bytes (0x38-0x3B) */
>  
> +/* Debug register unlock */
> +#define SIT9531X_PLL_REG_DEBUG		0xBD
> +#define SIT9531X_PLL_DEBUG_UNLOCK		0xC3
> +#define SIT9531X_PLL_DEBUG_LOCK			0x00

[Severity: Low]
This isn't a bug, but the same offset and the same two key values are
already spelled out in this header, with a comment saying the register
is page-independent:

/* Debug register (same offset, per-page) */
#define SIT9531X_REG_OUTSYS_DEBUG		SIT9531X_REG(0x03, 0xBD)
#define SIT9531X_DEBUG_UNLOCK_VAL		0xC3
#define SIT9531X_DEBUG_LOCK_VAL			0x00

Could the new PLL-page macros reuse those key values instead of
duplicating 0xBD/0xC3/0x00, so the two copies cannot diverge later?

> +
> +/*
> + * 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  /* [34: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

[Severity: Low]
Two small things here.

SIT9531X_PLL_REG_DBG_DATA_5, _6 and _7 have no user anywhere in the
driver at this commit, and still none at the end of the series (the
later sit9531x_dbg_sample() indexes SIT9531X_PLL_REG_DBG_DATA_0 + i).
Same for SIT9531X_DBG_DATA_BYTES at this commit, though that one does
gain a user in the fractional-frequency-offset patch.

The bit annotations also disagree: DATA_4 is "[34:32] + sign" while the
next line is "[47:40]", which leaves bits 35..39 unaccounted for.  The
core.c kernel-doc calls the value a "signed 35-bit code", but the decode
uses 35 magnitude bits plus a separate sign bit
(SIT9531X_TDC_SIGN_BIT 3, SIT9531X_TDC_MAG_HI_MASK GENMASK(2, 0)).
Could these be made consistent?

> +#define SIT9531X_PLL_REG_DBG_TRIGGER		0xD0  /* read to latch a sample */
> +

[ ... remaining regs.h definitions snipped ... ]

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260915000015.80480-1-arouhi%40sitime.com

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

* Re: [PATCH net-next v9 11/15] dpll: sit9531x: add support to get and set esync on pins
  2026-09-15  0:00 ` [PATCH net-next v9 11/15] dpll: sit9531x: add support to get and set esync on pins Ali Rouhi
@ 2026-09-17 15:01   ` netdev-bot+sashiko
  0 siblings, 0 replies; 33+ messages in thread
From: netdev-bot+sashiko @ 2026-09-17 15:01 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree,
	netdev, linux-kernel, kuba

Thank you for your contribution! Sashiko AI review found 7 potential
issue(s) to consider:

Critical: 0 · High: 2 · Medium: 5 · Low: 0

- [High] Enabling Embedded SYNC replaces the output pin's base frequency
  with a hard-coded 10 MHz carrier and caches it as the pin's current…
- [High] The esync enable path saves the pre-esync carrier best-effort
  and never verifies it, and the disable path then skips the restore…
- [Medium] SIT9531X_ESYNC_PULSE_DEFAULT (decimal 50) is written verbatim
  into the per-output PROG0 byte, which the same patch's register map…
- [Medium] sit9531x_dpll_output_pin_esync_get() reports only the software
  cache dpin->esync_freq (and derives esync->pulse from it) and never…
- [Medium] On the esync-disable error path, when
  sit9531x_output_freq_set() fails *after* it has already reprogrammed…
- [Medium] The esync enable rollback discards both cleanup results, so a
  failed rollback leaves the hardware in embedded-sync state while…
- [Medium] sit9531x_output_esync_program() writes the output's Hi-Z
  (mute) registers inside the programming sequence and then re-reads the…

--

dpll: sit9531x: add support to get and set esync on pins

This adds esync_get and esync_set to the sit9531x output pin ops, using the
existing divider programming plus a new per-output PROG0 pulse-control
write.  Enabling embedded sync reprograms the output divider and the pulse
generator in one programming sequence; disabling it clears the pulse
control and restores a saved rate.  Only outputs whose firmware node has
esync-control accept the operations.

A few questions about the new paths below.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index 8d857f1a0c890..46179a9e13c32 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -1654,6 +1654,103 @@ int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
>  	return rc;
>  }
>  

[ ... ]

> +int sit9531x_output_esync_program(struct sit9531x_dev *sitdev, u8 out_idx,
> +				  u8 pll_idx, u64 carrier, u8 pulse_ctrl)
> +{
> +	const struct sit9531x_chip_info *info = sitdev->info;
> +	u64 fvco, divo;
> +	bool muted;
> +	int rc, ret;
> +	u8 slot;
> +
> +	lockdep_assert_held(&sitdev->multiop_lock);
> +
> +	rc = sit9531x_output_divo_calc(sitdev, out_idx, pll_idx, carrier,
> +				       &fvco, &divo);
> +	if (rc)
> +		return rc;
> +
> +	slot = info->clkout_map[out_idx];
> +
> +	rc = sit9531x_prg_enter(sitdev);
> +	if (rc)
> +		return rc;
> +
> +	rc = sit9531x_output_divo_write(sitdev, out_idx, divo);
> +	if (!rc)
> +		rc = sit9531x_output_pulse_write(sitdev, out_idx, pulse_ctrl);
> +	/*
> +	 * Keep the mute the user asked for.  Embedded sync changes what the
> +	 * output carries, not whether it is driven, so an output muted
> +	 * through pin-state stays muted.
> +	 */
> +	if (!rc)
> +		rc = sit9531x_output_hiz_write(sitdev, slot,
> +					       !sitdev->out[out_idx].enabled);
> +
> +	ret = sit9531x_prg_commit(sitdev);
> +	if (ret && !rc)
> +		rc = ret;
> +
> +	if (!sit9531x_output_forced_hiz(sitdev, out_idx, &muted))
> +		sitdev->out[out_idx].enabled = !muted;

[Severity: Medium]
Should this readback failure mark the cached mute state stale?  The other
Hi-Z writers in the driver, sit9531x_output_disable() and
sit9531x_output_enable(), do:

	state_rc = sit9531x_output_forced_hiz(sitdev, index, &muted);
	if (!state_rc) {
		sitdev->out[index].enabled = !muted;
		sitdev->out[index].state_stale = false;
	} else {
		sitdev->out[index].state_stale = true;
		if (!rc)
			rc = state_rc;
	}

core.h documents state_stale as "the cached mute state could not be
confirmed against hardware and has to be read back before it is reported",
and sit9531x_dpll_output_pin_state_on_dpll_get() only reads through when
that flag is set.  Here the error is dropped, so an unconfirmed value is
reported as authoritative.

The same cached bit is also the input to the Hi-Z write a few lines above:

	rc = sit9531x_output_hiz_write(sitdev, slot,
				       !sitdev->out[out_idx].enabled);

so can an unconfirmed cache change the drive state of an output as a side
effect of an embedded-sync request?

> +
> +	if (rc)
> +		return rc;
> +
> +	rc = sit9531x_output_phase_flush(sitdev, pll_idx);
> +	if (rc)
> +		return rc;
> +
> +	sitdev->out[out_idx].freq = div64_u64(fvco, divo);
> +
> +	/*
> +	 * The delay registers count VCO cycles against the output period in
> +	 * force when they were written, so the carrier change re-times a
> +	 * phase adjust the same way a frequency set does.
> +	 */
> +	if (sitdev->out[out_idx].phase_adj)
> +		return sit9531x_output_phase_adjust_set(sitdev, out_idx,
> +							sitdev->out[out_idx].phase_adj);
> +
> +	return 0;
> +}
> +

[ ... ]

> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index 78034b7f089cc..0cad081eb5995 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -22,9 +22,21 @@
>  #include "prop.h"
>  #include "regs.h"
>  
> -#define SIT9531X_ESYNC_FREQ_10MHZ	10000000ULL
> +#define SIT9531X_ESYNC_FREQ_1HZ	DPLL_PIN_FREQUENCY_1_HZ
> +#define SIT9531X_ESYNC_FREQ_10MHZ	DPLL_PIN_FREQUENCY_10_MHZ
>  #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_1HZ),
> +};
> +

[ ... ]

> @@ -726,6 +738,12 @@ sit9531x_dpll_output_pin_frequency_set(const struct dpll_pin *pin,
>  	actual_pll = sitdev->out[dpin->id].pll_idx;
>  
>  	mutex_lock(&sitdev->multiop_lock);
> +	if (dpin->esync_freq) {
> +		mutex_unlock(&sitdev->multiop_lock);
> +		NL_SET_ERR_MSG(extack,
> +			       "Disable embedded sync on this pin before changing frequency");
> +		return -EBUSY;
> +	}
>  	rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll,
>  				      frequency);
>  	mutex_unlock(&sitdev->multiop_lock);

[ ... ]

> @@ -869,6 +887,138 @@ sit9531x_dpll_output_pin_phase_adjust_set(const struct dpll_pin *pin,
>  	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;
> +	struct sit9531x_dpll *sitdpll = dpll_priv;
> +	struct sit9531x_dev *sitdev = sitdpll->dev;
> +
> +	if (!sit9531x_dpll_esync_pin_supported(dpin))
> +		return -EOPNOTSUPP;
> +
> +	mutex_lock(&sitdev->multiop_lock);
> +	esync->range = sit9531x_esync_ranges;
> +	esync->range_num = ARRAY_SIZE(sit9531x_esync_ranges);
> +	esync->freq = dpin->esync_freq;

[Severity: Medium]
This getter reports only the software cache and never reads PROG0 back,
while the sibling sit9531x_dpll_output_pin_frequency_get() does consult the
device through sit9531x_output_freq_get().

dpin->esync_freq starts at 0 (the pin struct is zeroed at allocation and
registration only copies esync_control), so on a device whose NVM already
has the marker programmed the pin reports DPLL_A_PIN_ESYNC_FREQUENCY as 0.

Can that marker then be cleared?  The disable branch short-circuits on the
same cache:

		if (!dpin->esync_freq) {
			mutex_unlock(&sitdev->multiop_lock);
			return 0;
		}

and dpll_pin_esync_set() in dpll_netlink.c drops the request before it
reaches the driver:

	if (freq == esync.freq)
		return 0;

The new -EBUSY interlock in sit9531x_dpll_output_pin_frequency_set() keys
off the same uninitialized value, so it would not cover a DIVO change while
a marker the driver did not program itself is active.  Would seeding the
cache from PROG0 during probe address both?

> +	/*
> +	 * The hardware PROG0 byte is programmed with the same literal that the
> +	 * ABI reports as pulse percent for the supported 1 Hz mode. When esync
> +	 * is disabled, report pulse as zero to avoid implying an active marker.
> +	 */
> +	esync->pulse = dpin->esync_freq ? SIT9531X_ESYNC_PULSE_DEFAULT : 0;

[Severity: Medium]
Is one literal valid as both the register value and the ABI percentage?
The register comment added by this patch in regs.h says:

 * Per-output pulse-count control byte used in SYSREF / SYNCB modes.

while Documentation/netlink/specs/dpll.yaml defines esync-pulse as "A ratio
of high to low state of a SYNC signal pulse embedded into base clock
frequency.  Value is in percents."

SIT9531X_ESYNC_PULSE_DEFAULT is handed to sit9531x_output_pulse_write() as
the raw PROG0 byte and returned here as the percentage, with no conversion
either way.

With the 10 MHz carrier this code programs and a 1 Hz marker, half a period
would be 5000000 carrier cycles, which does not fit an 8-bit field, so the
commit message claim:

  "The pulse width is fixed at half the period, which is what the device
   produces"

does not seem expressible in a pulse-count register.  Should the percentage
be converted to a pulse count on set and back on get?

> +	mutex_unlock(&sitdev->multiop_lock);
> +
> +	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);
> +
> +	if (!freq) {
> +		u64 prev_freq = dpin->esync_prev_freq;
> +		u8 pulse = SIT9531X_ESYNC_PULSE_DEFAULT;
> +
> +		if (!dpin->esync_freq) {
> +			mutex_unlock(&sitdev->multiop_lock);
> +			return 0;
> +		}
> +		rc = sit9531x_output_pulse_ctrl_set(sitdev, dpin->id, 0);
> +		if (!rc && prev_freq)
> +			rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll, prev_freq);
> +		if (rc)
> +			sit9531x_output_pulse_ctrl_set(sitdev, dpin->id, pulse);
> +		if (!rc)
> +			dpin->esync_freq = 0;

[Severity: Medium]
Can this rollback leave the reported state disagreeing with the hardware?

sit9531x_output_freq_set() can return an error after it has already
committed the new divider: the sit9531x_output_phase_flush() step and the
trailing sit9531x_output_phase_adjust_set() both run after
sit9531x_prg_commit().  In that case the code above re-writes the marker and
leaves dpin->esync_freq at 1 Hz, so the pin advertises esync active while
the carrier is already back on the user's rate with only a stale marker.

Getting back in sync afterwards looks awkward: frequency_set is refused by
the new -EBUSY guard, and a repeated esync = 1 Hz request is discarded by
dpll_pin_esync_set() because it equals the cached value, so only repeating
the disable helps.

> +		mutex_unlock(&sitdev->multiop_lock);
> +		if (rc)
> +			NL_SET_ERR_MSG(extack,
> +				       "Failed to turn embedded sync off on this output");
> +		return rc;
> +	}
> +
> +	if (freq != SIT9531X_ESYNC_FREQ_1HZ) {
> +		mutex_unlock(&sitdev->multiop_lock);
> +		NL_SET_ERR_MSG(extack,
> +			       "Only 1 Hz embedded-sync frequency is supported");
> +		return -EINVAL;
> +	}
> +
> +	if (dpin->esync_freq == SIT9531X_ESYNC_FREQ_1HZ) {
> +		mutex_unlock(&sitdev->multiop_lock);
> +		return 0;
> +	}
> +
> +	if (!dpin->esync_freq) {
> +		/*
> +		 * Remember the carrier to restore.  The cached rate is zero
> +		 * for an output whose firmware node lists no frequency and
> +		 * which has never been set, so read the divider rather than
> +		 * leave the output parked on the esync carrier at disable.
> +		 */
> +		dpin->esync_prev_freq = sitdev->out[dpin->id].freq;
> +		if (!dpin->esync_prev_freq)
> +			sit9531x_output_freq_get(sitdev, dpin->id,
> +						 &dpin->esync_prev_freq);
> +	}

[Severity: High]
The return value of sit9531x_output_freq_get() is dropped here.  Can
esync_prev_freq stay 0 in exactly the case the comment says it must not?

sit9531x_output_freq_get() assigns *frequency only on its success path:

	if (!divo)
		return -ENODEV;

	*frequency = div64_u64(fvco, divo);

It also returns -ENODEV for an out-of-range pll_idx or unresolvable Fvco,
and propagates any I2C error from the five DIVO byte reads.  In all of those
cases esync_prev_freq keeps its prior value of 0 and the enable still goes
on to commit the esync carrier.

The disable path then skips the restore and reports success anyway:

		rc = sit9531x_output_pulse_ctrl_set(sitdev, dpin->id, 0);
		if (!rc && prev_freq)
			rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll, prev_freq);
		...
		if (!rc)
			dpin->esync_freq = 0;

With prev_freq == 0 the divider stays on the esync carrier, the marker is
switched off, esync_freq is cleared and 0 is returned to userspace, so the
output is left running at a rate nobody asked for and nothing reports it.
The enable-failure rollback below carries the same condition.

sit9531x_output_phase_adjust_set() checks the return value of the same
helper.  Should this call check it too, and should a failed save refuse the
enable instead of continuing?

> +
> +	/*
> +	 * Carrier, marker and enable go into the device in one programming
> +	 * sequence.  Without the pulse generator (PROG0 PULSE_CTRL) the
> +	 * output would carry 10 MHz and no marker at all;
> +	 * SIT9531X_ESYNC_PULSE_DEFAULT is the duty esync_get advertises for
> +	 * the 1 Hz embedded-sync signal.
> +	 */
> +	rc = sit9531x_output_esync_program(sitdev, dpin->id, actual_pll,
> +					   SIT9531X_ESYNC_FREQ_10MHZ,
> +					   SIT9531X_ESYNC_PULSE_DEFAULT);

[Severity: High]
Enabling embedded sync here programs a fixed 10 MHz carrier instead of
adding a marker to the rate the pin is already running.  Is that the
intent?

The commit message says:

  "so enabling it sets the output to 1 Hz and disabling it restores the
   requested rate."

but the carrier passed in is SIT9531X_ESYNC_FREQ_10MHZ, so neither 1 Hz nor
the rate the user configured ends up in DIVO.

sit9531x_output_esync_program() also records that carrier as the pin rate:

	sitdev->out[out_idx].freq = div64_u64(fvco, divo);

and sit9531x_dpll_output_pin_frequency_get() reads DIVO back from the
device, so after an esync enable the pin reports 10 MHz.  The esync-control
example in Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
advertises only:

	esync-control;
	supported-frequencies-hz = /bits/ 64 <156250000>;

so the same pin would then report a current frequency that is not in its own
DPLL_A_PIN_FREQUENCY_SUPPORTED set and that frequency_set would reject.

Documentation/driver-api/dpll.rst describes Embedded SYNC as a SYNC signal
embedded into the pin's base frequency, with the capability "always related
to a given base frequency".  Would it be better to offer esync only when the
base frequency already matches what the device needs, rather than
reprogramming the base frequency as part of the esync request?

> +	if (rc) {
> +		sit9531x_output_pulse_ctrl_set(sitdev, dpin->id, 0);
> +		if (dpin->esync_prev_freq)
> +			sit9531x_output_freq_set(sitdev, dpin->id,
> +						 actual_pll,
> +						 dpin->esync_prev_freq);
> +	}

[Severity: Medium]
Both cleanup calls have their return values discarded, and esync_freq stays
0 either way.

sit9531x_output_esync_program() can fail after the divider and PROG0 have
already been committed, since sit9531x_output_phase_flush() and
sit9531x_output_phase_adjust_set() run after sit9531x_prg_commit().  If the
two compensating writes then also fail, can the device keep the marker
enabled and the 10 MHz carrier while the driver reports esync disabled?

In that state a later esync = 0 request is dropped by dpll_pin_esync_set()
as already satisfied, so the marker cannot be cleared through the ABI.
Should a failed rollback be propagated, or the cached state marked as
unconfirmed?

> +
> +	mutex_unlock(&sitdev->multiop_lock);
> +
> +	if (rc) {
> +		NL_SET_ERR_MSG(extack,
> +			       "Failed to program embedded sync on this output");
> +		return rc;
> +	}
> +
> +	dpin->esync_freq = SIT9531X_ESYNC_FREQ_1HZ;
> +
> +	return 0;
> +}
> +

[ ... ]

> diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
> index f40762f06403b..9d515e109feb5 100644
> --- a/drivers/dpll/sit9531x/regs.h
> +++ b/drivers/dpll/sit9531x/regs.h
> @@ -230,6 +230,13 @@
>  #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
> +

Thanks for the patch.

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260915000015.80480-1-arouhi%40sitime.com

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

* Re: [PATCH net-next v9 13/15] dpll: sit9531x: add support to get fractional frequency offset
  2026-09-15  0:00 ` [PATCH net-next v9 13/15] dpll: sit9531x: add support to get fractional frequency offset Ali Rouhi
@ 2026-09-17 15:01   ` netdev-bot+sashiko
  0 siblings, 0 replies; 33+ messages in thread
From: netdev-bot+sashiko @ 2026-09-17 15:01 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree,
	netdev, linux-kernel, kuba

Thank you for your contribution! Sashiko AI review found 5 potential
issue(s) to consider:

Critical: 0 · High: 1 · Medium: 1 · Low: 3

- [High] sit9531x_divn_runtime() feeds an unvalidated 48-bit sampled
  numerator and a raw sampled denominator (only checked for != 0) into…
- [Medium] sit9531x_dbg_sample() writes SIT9531X_PLL_DEBUG_UNLOCK (0xC3)
  to the per-PLL debug key register SIT9531X_PLL_REG_DEBUG (0xBD) but…
- [Low] The documentation block for sit9531x_dbg_sample() lists @sitdev,
  @pll_idx, @read_code, @buf and @len, but the implemented signature…
- [Low] SIT9531X_DIVN_RT_INT_HI_BIT is defined in regs.h under a comment
  that describes it as belonging to the integer part of the DIVN tap…
- [Low] The kernel-doc of sit9531x_pll_ffo_ppt() states 'Return: 0 on
  success, -ENODATA when DIVN is not programmed, <0 on error', but the…

--

dpll: sit9531x: add support to get fractional frequency offset

This adds a fractional frequency offset readback for the sit9531x DPLL
inputs, derived from the difference between the DIVN the digital loop is
running and the DIVN the configuration registers hold.  The running value
is sampled through the same latched debug window that the phase offset
readback uses.

A few questions on the new code below.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index 356d1eac4ec54..88bfa63b29755 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -1224,6 +1224,76 @@ static int sit9531x_is_xo_doubler_enabled(struct sit9531x_dev *sitdev)
>  	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, bool low_freq_clk,
> +			       u8 *buf, unsigned int len)

[Severity: Low]
This isn't a bug, but the comment block for sit9531x_dbg_sample() documents
five parameters (sitdev, pll_idx, read_code, buf, len) while the function
takes six, with bool low_freq_clk sitting between read_code and buf.  Since
the clock-select argument is the one the block goes on to explain in prose,
should it get a @low_freq_clk entry too?

> +{
> +	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;

[Severity: Medium]
Should this helper close the debug window before returning?

SIT9531X_PLL_REG_DEBUG is written with SIT9531X_PLL_DEBUG_UNLOCK here, and
none of the four error returns below, nor the final return 0, writes
SIT9531X_PLL_DEBUG_LOCK back.

The existing user of the same window, sit9531x_phase_offset_read(), funnels
every exit through a relock label and states what the open window means:

	relock:
		/*
		 * Close the debug window again.  The key register opens every debug
		 * register on this PLL while it holds the unlock value, and this read
		 * runs on every pin-get of a connected input, so leaving it open
		 * would mean normal monitoring permanently unlocks the block.
		 */
		lock_rc = sit9531x_write_pll_u8(sitdev, pll_idx,
						SIT9531X_PLL_REG_DEBUG,
						SIT9531X_PLL_DEBUG_LOCK);

The new path sit9531x_dpll_input_pin_ffo_get() ->
sit9531x_pll_ffo_ppt() -> sit9531x_divn_runtime() -> sit9531x_dbg_sample()
runs on every pin-get of a connected input and calls the helper twice, and
sit9531x_divn_runtime() returns straight out on the first error, so nothing
above it re-locks either.  Does that leave the PLL's debug block unlocked
after every frequency offset read?

> +
> +	/*
> +	 * Select the debug clock this tap needs.  The device never clears
> +	 * the bit, so a read that left it to whatever the previous one set
> +	 * would depend on the order the taps happened to be read in.  Taps
> +	 * below 200 kHz need the slow clock; the divider taps do not.
> +	 */

[ ... ]

> @@ -1291,6 +1361,130 @@ static int sit9531x_divn_static(struct sit9531x_dev *sitdev, u8 pll_idx,
>  	return 0;
>  }
>  

[ ... ]

> +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;

[ ... ]

> +	for (i = 5; i >= 0; i--)
> +		fracd = (fracd << 8) | buf[i];
> +
> +	fracd |= (u64)(buf[6] & SIT9531X_DIVN_RT_INT_HI_BIT) << 48;
> +
> +	/*
> +	 * sit9531x_divn_fixed() drops the fractional part when the
> +	 * denominator is zero.  That is right for a configuration register
> +	 * that was never programmed, but a running loop reading zero means
> +	 * the sample is unusable, and returning the whole part alone would
> +	 * put a wrong DIVN into the frequency offset without saying so.
> +	 */
> +	if (!fracd)
> +		return -ENODATA;
> +
> +	*divn = sit9531x_divn_fixed(int_part,
> +				    sign_extend64(fracn_raw,
> +						  SIT9531X_DIVN_RT_NUM_BITS - 1),
> +				    fracd);

[Severity: High]
Can a small but nonzero denominator sample trap here?

The only check on the sampled pair is "if (!fracd) return -ENODATA;", so any
fracd in 1..2^49-1 is passed on together with a numerator of up to 2^47 in
magnitude after sign_extend64().  sit9531x_divn_fixed() then does:

	frac = mul_u64_u64_div_u64(abs(fracn), SIT9531X_DIVN_SCALE, fracd);

With SIT9531X_DIVN_SCALE at 1e12, abs(fracn) near 2^47 gives a product of
about 1.4e26, so the quotient exceeds U64_MAX for any fracd below roughly
7.6e6.  On x86-64 that is not a wrong value but a divide-error exception, per
arch/x86/include/asm/div64.h:

	/*
	 * Will generate an #DE when the result doesn't fit u64, could fix with an
	 * __ex_table[] entry when it becomes an issue.
	 */
	...
		asm ("divq %[div]" : "+a" (rax), "+d" (rdx) : [div] "rm" (div));

On architectures using the generic helper the result comes back as ~0 and is
reported to userspace as a frequency offset instead.

Both operands come from the I2C part through the latched debug window, and
the code already accepts that the tap may return something that is not a
running loop when it special-cases fracd == 0.  Would rejecting samples where
abs(fracn) >= fracd (or otherwise bounding the ratio) before calling
sit9531x_divn_fixed() be appropriate?  sit9531x_pll_ffo_ppt() adds exactly
this class of guard for the configured divider:

	if (configured < SIT9531X_DIVN_SCALE)
		return -ENODATA;

Note the pre-existing sit9531x_divn_static() path only supplies a 32-bit
numerator, so the runtime tap widens the range of inputs that can overflow
that division considerably.

> +
> +	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

[ ... ]

> + * Caller must hold sitdev->multiop_lock.
> + *
> + * Return: 0 on success, -ENODATA when DIVN is not programmed, <0 on
> + * error.
> + */

[Severity: Low]
This isn't a bug, but the documented return contract of
sit9531x_pll_ffo_ppt() lists -ENODATA only for an unprogrammed DIVN, while
two other paths return it as well:

sit9531x_divn_runtime() returns -ENODATA when the sampled runtime
denominator reads zero, which its own comment describes as "the sample is
unusable" rather than an unprogrammed divider.

sit9531x_pll_ffo_ppt() itself returns -ENODATA when the configured divider
is below one whole unit.

Could the Return: line cover those cases too?

[ ... ]

> diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
> index 08eae8a2c279a..78e8a47e96ab0 100644
> --- a/drivers/dpll/sit9531x/regs.h
> +++ b/drivers/dpll/sit9531x/regs.h
> @@ -309,6 +309,18 @@
>  #define SIT9531X_TDC_SIGN_BIT		3
>  #define SIT9531X_TDC_MAG_HI_MASK	GENMASK(2, 0)
>  
> +/*
> + * 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)

[Severity: Low]
This isn't a bug, but SIT9531X_DIVN_RT_INT_HI_BIT is named and commented as
the high bit of the integer part of the DIVN tap, and it is used that way in
sit9531x_divn_runtime():

	int_part = buf[6] | ((u32)(buf[7] & SIT9531X_DIVN_RT_INT_HI_BIT) << 8);

The same macro is then reused as the bit-48 mask of the denominator, which
is a different byte of a different tap:

	fracd |= (u64)(buf[6] & SIT9531X_DIVN_RT_INT_HI_BIT) << 48;

Would a separate SIT9531X_DIVN_RT_DEN_HI_BIT make the two fields
distinguishable, so a later change to the integer field width does not also
change the denominator read?

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260915000015.80480-1-arouhi%40sitime.com

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

* Re: [PATCH net-next v9 14/15] dpll: sit9531x: model the inter-PLL sync net as a pair of pins
  2026-09-15  0:00 ` [PATCH net-next v9 14/15] dpll: sit9531x: model the inter-PLL sync net as a pair of pins Ali Rouhi
@ 2026-09-17 15:01   ` netdev-bot+sashiko
  0 siblings, 0 replies; 33+ messages in thread
From: netdev-bot+sashiko @ 2026-09-17 15:01 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree,
	netdev, linux-kernel, kuba

Thank you for your contribution! Sashiko AI review found 8 potential
issue(s) to consider:

Critical: 0 · High: 2 · Medium: 3 · Low: 3

- [High] Unbalanced acquire/release of the exclusive INTSYNC net:
  sit9531x_intsync_enable()/disable() never record ownership; the only…
- [High] The new sit9531x_dpll_intsync_dst_pin_ops wires .prio_set =
  sit9531x_dpll_input_pin_prio_set, which ends with 'while…
- [Medium] sit9531x_intsync_enable() and sit9531x_intsync_disable() write
  the per-page debug key (offset SIT9531X_PLL_REG_DEBUG = 0xBD, value…
- [Medium] Source misattribution after a partially failed disable:
  sit9531x_intsync_disable() clears and commits the global enable bit…
- [Medium] The new source-side loopback check can be bypassed through a
  stale cache: a failed INTSYNC destination removal still rebuilds…
- [Low] The patch adds a second, identical prototype for
  sit9531x_input_prio_present() to core.h; the same declaration already…
- [Low] The patch contains a comment-only rewrite of the
  sit9531x_output_phase_adjust_set() kerneldoc (slot-vs-logical-index…
- [Low] sit9531x_dpll_intsync_dst_state_on_dpll_set() calls
  sit9531x_input_prio_add(), which returns -ENOSPC when the priority…

--

dpll: sit9531x: model the inter-PLL sync net as a pair of pins

The patch exposes the chip's internal inter-PLL sync (INTSYNC) net as two
DPLL pins: an output pin on the PLL that drives the net and an input pin on
each PLL that can select it as a reference.  New core helpers program the
source PLL's EXT page and a detector re-reads which PLL currently drives the
net into sitdev->intsync_src.

A few questions below about the ownership bookkeeping, the debug-key
handling and the new destination pin ops.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index 88bfa63b29755..b15859890caa7 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -2024,12 +2024,13 @@ int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
>   *   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).
> + * Slots 0-5 live on Page 3, slots 6-11 on Page 4, with each slot's
> + * block at base = 0x15 + 16 * (slot % 6); the slot is the physical
> + * output position from clkout_map[], not the logical output index.
>   *
> - * 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.
> + * The chip only supports unsigned positive delay.  Requests are folded
> + * modulo one output period: positive delays wrap naturally and a negative
> + * phase adjustment (advance) is rendered as (T_out - |phase|).
>   */

[Severity: Low]
This isn't a bug, but is this hunk meant to be part of this patch?  It only
rewrites the documentation block above sit9531x_output_phase_adjust_set()
(slot versus logical index, phase folding wording) and no code in that
function changes, while the commit message describes only the INTSYNC pin
model.  Could it move to its own patch so a reader does not have to check
whether phase-adjust behaviour changed here?

> @@ -2282,6 +2283,225 @@ int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
>  	return 0;
>  }
>  

[ ... ]

> +int sit9531x_intsync_src_detect(struct sit9531x_dev *sitdev)
> +{
> +	s8 src = -1;
> +	u8 global;
> +	u8 pll, ext_page;
> +	int rc, i;
> +
> +	lockdep_assert_held(&sitdev->multiop_lock);
> +
> +	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &global);
> +	if (rc)
> +		return rc;
> +
> +	if (!(global & BIT(SIT9531X_INTSYNC_EN_BIT))) {
> +		sitdev->intsync_src = -1;
> +		return 0;
> +	}
> +
> +	for (pll = 0; pll < SIT9531X_NUM_PLLS; pll++) {
> +		ext_page = SIT9531X_PLL_EXT_PAGE(pll);
> +
> +		for (i = 0; i < ARRAY_SIZE(intsync_config); i++) {
> +			u16 reg;
> +			u8 val;
> +
> +			reg = SIT9531X_REG(ext_page, intsync_config[i].offset);
> +
> +			rc = sit9531x_read_u8(sitdev, reg, &val);
> +			if (rc)
> +				return rc;
> +			if (val != intsync_config[i].en_val)
> +				break;
> +		}
> +
> +		if (i == ARRAY_SIZE(intsync_config)) {
> +			/*
> +			 * Only one PLL can drive the net.  If a second
> +			 * one matches, the registers are not describing
> +			 * a state this driver put the device in, so say
> +			 * so rather than pick silently.
> +			 */
> +			if (src < 0)
> +				src = pll;
> +			else
> +				dev_warn(sitdev->dev,
> +					 "PLL%c also matches the INTSYNC source pattern; keeping PLL%c\n",
> +					 'A' + pll, 'A' + src);
> +		}
> +	}
> +
> +	sitdev->intsync_src = src;
> +
> +	return 0;
> +}

[Severity: Medium]
Can a partially failed disable make this detector attribute the net to the
wrong PLL?

sit9531x_intsync_disable() clears and commits the global enable bit first,
and only afterwards unlocks the EXT page and writes the eight dis_val
bytes, each with a bare "if (rc) return rc;".  If one of those writes
fails, that PLL's EXT page still holds the complete en_val pattern.

sit9531x_intsync_src_detect() then sees the cleared global bit:

	if (!(global & BIT(SIT9531X_INTSYNC_EN_BIT))) {
		sitdev->intsync_src = -1;
		return 0;
	}

so the leftover pattern is never noticed, and a retry of DISCONNECTED in
sit9531x_dpll_intsync_src_state_on_dpll_set() short-circuits on
"if (sitdev->intsync_src != sitdpll->id) break;".

When a later CONNECTED on another PLL re-asserts the global bit, the loop
here keeps the first matching PLL, so the stale lower-numbered PLL shadows
the PLL that was actually enabled: the newly connected pin reports
DISCONNECTED and cannot be disconnected, an unrelated PLL's source pin
reports CONNECTED, and two PLLs carry the source pattern for one net.
Would clearing the EXT page before the global bit, or recording an
"unknown/needs cleanup" state, be preferable to only warning here?

> +
> +/*
> + * 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)
> +{

[ ... ]

> +	/* Unlock debug on EXT page */
> +	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(ext_page, SIT9531X_PLL_REG_DEBUG),
> +			       SIT9531X_PLL_DEBUG_UNLOCK);
> +	if (rc)
> +		goto err_disable;

[Severity: Medium]
Should the debug key be written back to SIT9531X_PLL_DEBUG_LOCK before
returning?  Both sit9531x_intsync_enable() and sit9531x_intsync_disable()
write SIT9531X_PLL_DEBUG_UNLOCK to SIT9531X_PLL_REG_DEBUG on the source
PLL's EXT page and never restore the lock value, on success or on any error
path (the enable error path calls disable(), which also only unlocks), so
that page's protected debug registers stay open for the lifetime of the
device after any INTSYNC source set or clear.

The existing sit9531x_phase_offset_read() in the same file always closes
the window again:

relock:
	/*
	 * Close the debug window again.  The key register opens every debug
	 * register on this PLL while it holds the unlock value ...
	 */
	lock_rc = sit9531x_write_pll_u8(sitdev, pll_idx,
					SIT9531X_PLL_REG_DEBUG,
					SIT9531X_PLL_DEBUG_LOCK);

Is there a reason the INTSYNC helpers do not follow the same unlock/relock
protocol?

> +
> +	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)
> +			goto err_disable;
> +	}
> +
> +	/* 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)
> +		goto err_disable;
> +
> +	return 0;
> +
> +err_disable:
> +	/*
> +	 * The global enable is already set at this point.  The caller only
> +	 * records the source PLL when this function succeeds, so nothing
> +	 * else will ever clear the bit: undo it here rather than leave the
> +	 * net asserted with a half-written EXT page.
> +	 */

[Severity: High]
Is the caller contract described in this comment actually implemented?
"The caller only records the source PLL when this function succeeds"
suggests the caller assigns sitdev->intsync_src on success, but
sit9531x_dpll_intsync_src_state_on_dpll_set() never assigns it; the only
writer of the field is sit9531x_intsync_src_detect(), whose failure is
downgraded to a dev_warn() there.  See the comment on that call site below.

[ ... ]

> +int sit9531x_intsync_disable(struct sit9531x_dev *sitdev, u8 src_pll_idx)
> +{

[ ... ]

> +	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;
> +}

[ ... ]

> diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
> index 1f77ff28ec814..5275733a3b4a9 100644
> --- a/drivers/dpll/sit9531x/core.h
> +++ b/drivers/dpll/sit9531x/core.h
> @@ -252,6 +252,8 @@ 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);
> +bool sit9531x_input_prio_present(struct sit9531x_dev *sitdev,
> +				 u8 pll_idx, u8 input_idx);
>  int sit9531x_input_prio_remove(struct sit9531x_dev *sitdev, u8 pll_idx,
>  			       u8 input_idx);

[Severity: Low]
This isn't a bug, but core.h already declares sit9531x_input_prio_present()
with exactly this prototype a few lines above, under the input priority
comment, before this patch.  Is this second declaration left over from a
rebase?  It can be dropped.

[ ... ]

> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index 2a973364a9cd2..3efe6fc90701d 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -792,8 +806,222 @@ sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,

[ ... ]

> +	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.
> +		 */
> +		if (sit9531x_input_prio_present(sitdev, sitdpll->id,
> +						sit9531x_input_hw_src(SIT9531X_INTSYNC_PIN_ID))) {
> +			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);
> +		break;

[Severity: Medium]
Can this loopback check be bypassed by a stale priority mask?

sit9531x_input_prio_present() reads only chan->prio_mask:

	return !!(sitdev->chan[pll_idx].prio_mask & BIT(input_idx));

and sit9531x_prio_table_commit() rebuilds that mask from the *intended*
table even when a slot write failed:

	sit9531x_prio_mask_build(sitdev, pll_idx, srcs);

So after a failed DISCONNECTED on the INTSYNC destination pin of a PLL
(sit9531x_dpll_intsync_dst_state_on_dpll_set() -> sit9531x_input_prio_remove()
-> sit9531x_prio_table_commit()), the mask says INTSYNC is absent while the
hardware slot still holds it.  A CONNECTED request on the same PLL's source
pin before the next periodic sit9531x_chan_state_fetch() re-read then passes
this check and calls sit9531x_intsync_enable(), which is the self-reference
the check exists to prevent.  Would reading the table back here, or not
updating the mask on a partial commit, close that window?

> +	case DPLL_PIN_STATE_DISCONNECTED:
> +		if (sitdev->intsync_src != sitdpll->id)
> +			break;
> +		rc = sit9531x_intsync_disable(sitdev, sitdpll->id);
> +		break;
> +	default:
> +		rc = -EINVAL;
> +		break;
> +	}
> +
> +	/*
> +	 * Re-scan hardware after source state transitions so cache follows
> +	 * partially failed enable/disable paths as closely as possible.
> +	 */
> +	if (state == DPLL_PIN_STATE_CONNECTED ||
> +	    state == DPLL_PIN_STATE_DISCONNECTED)
> +		detect_rc = sit9531x_intsync_src_detect(sitdev);
> +	/*
> +	 * The refresh only re-reads what the device now shows.  Failing
> +	 * the request because that read hit a bus error would tell
> +	 * userspace the enable did not happen when it did.
> +	 */
> +	if (detect_rc)
> +		dev_warn(sitdev->dev,
> +			 "INTSYNC source cache not refreshed: %d\n",
> +			 detect_rc);

[Severity: High]
Can the net become impossible to release when this refresh fails?

sit9531x_intsync_enable() and sit9531x_intsync_disable() never touch
sitdev->intsync_src, so this detect call is the only thing that records
ownership.  In sit9531x_intsync_src_detect() any read error returns before
the assignment:

			rc = sit9531x_read_u8(sitdev, reg, &val);
			if (rc)
				return rc;
	...
	sitdev->intsync_src = src;

So one i2c read error after a successful enable leaves intsync_src == -1
while the hardware drives the net.  From then on:

  - the source pin's state_on_dpll_get reports DISCONNECTED,
  - a DISCONNECTED request hits "if (sitdev->intsync_src != sitdpll->id)
    break;" and never calls sit9531x_intsync_disable(),
  - the "already sourced by another PLL" check passes, so a second PLL can
    be programmed onto the same single net,
  - the destination-side "PLL cannot lock to the INTSYNC it drives" guard
    stops matching.

sit9531x_dev_periodic_work() refreshes only ref[] and chan[] and never
re-runs the INTSYNC detect, so the divergence is not repaired later.  The
mirror case (successful disable, failed detect) keeps a stale positive
value and refuses every other PLL with -EBUSY.

Would recording ownership directly in the enable/disable helpers on success
(and clearing it on success of disable), instead of relying on a fallible
readback whose error is only warned about, avoid this?

>  /* ---- INTSYNC destination (input) pin ---- */
>  

[ ... ]

> +	switch (state) {
> +	case DPLL_PIN_STATE_DISCONNECTED:
> +		rc = sit9531x_input_prio_remove(sitdev, sitdpll->id, hw_src);
> +		break;
> +	case DPLL_PIN_STATE_CONNECTED:
> +		NL_SET_ERR_MSG(extack,
> +			       "Device selects its reference by priority; use selectable");
> +		rc = -EOPNOTSUPP;
> +		break;
> +	case DPLL_PIN_STATE_SELECTABLE:
> +		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 == -EBUSY)
> +		NL_SET_ERR_MSG(extack,
> +			       "Only source left in the priority table; it cannot be emptied");
> +	else if (rc && rc != -EINVAL && rc != -EOPNOTSUPP)
> +		NL_SET_ERR_MSG(extack, "Failed to set INTSYNC input state");

[Severity: Low]
Should -ENOSPC get its own message here?  sit9531x_input_prio_add()
returns -ENOSPC when the priority table is full of unique sources, and this
tail reports that as "Failed to set INTSYNC input state", which reads like
a bus failure.

The physical-input peer, sit9531x_dpll_input_pin_state_on_dpll_set(), calls
the same helper and maps it explicitly:

	else if (rc == -ENOSPC)
		NL_SET_ERR_MSG(extack,
			       "Priority table is full of unique sources on this PLL");

The comment above this function says it "accepts and refuses what a
physical input does", so is the divergence intentional?

> +
> +	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,
> +};

[Severity: High]
Does wiring .prio_set here make a recursive dpll_lock acquisition reachable
on four more pins?

sit9531x_dpll_input_pin_prio_set() ends with:

	while (changed_cnt--)
		dpll_pin_change_ntf(changed[changed_cnt]);

and dpll_pin_change_ntf() takes the lock itself:

int dpll_pin_change_ntf(struct dpll_pin *pin)
{
	int ret;

	mutex_lock(&dpll_lock);

but every dpll_pin_ops callback already runs with dpll_lock held:
DPLL_CMD_PIN_SET uses .pre_doit = dpll_pin_pre_doit, which does
mutex_lock(&dpll_lock) and returns with it held until dpll_pin_post_doit(),
and dpll_pin_set_from_nlattr() -> dpll_pin_prio_set() invokes ops->prio_set
under it.  A DPLL_CMD_PIN_SET carrying DPLL_A_PIN_PRIO would then
re-acquire a non-recursive mutex the task already holds.

The path was introduced earlier in this series by "dpll: sit9531x: add
support to get and set priority on input pins", and this patch attaches the
same prio_set to the INTSYNC destination pin of each PLL, and makes that
pin a sibling candidate in the loop that fills changed[], so it can also
raise changed_cnt for the physical input pins.  Note the driver's own
comment in sit9531x_dev_periodic_work() states that "Those notification
helpers take DPLL-subsystem locks that are already held when our callbacks
are invoked from netlink context".

Would __dpll_pin_change_ntf(), which has lockdep_assert_held(&dpll_lock),
be the right helper here, or should the notification not be emitted from
inside the callback at all?

[ ... ]

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260915000015.80480-1-arouhi%40sitime.com

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

* Re: [PATCH net-next v9 15/15] dpll: sit9531x: allow the device tree to override two board facts
  2026-09-15  0:00 ` [PATCH net-next v9 15/15] dpll: sit9531x: allow the device tree to override two board facts Ali Rouhi
@ 2026-09-17 15:02   ` netdev-bot+sashiko
  0 siblings, 0 replies; 33+ messages in thread
From: netdev-bot+sashiko @ 2026-09-17 15:02 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi, devicetree,
	netdev, linux-kernel, kuba

Thank you for your contribution! Sashiko AI review found 3 potential
issue(s) to consider:

Critical: 0 · High: 1 · Medium: 1 · Low: 1 (1)

- [High] The new parse-time band check in sit9531x_parse_board_config()
  (drivers/dpll/sit9531x/core.c:3912-3921, via sit9531x_fvco_in_band()…
- [Medium] sit9531x_parse_board_config()
  (drivers/dpll/sit9531x/core.c:3934-3964) pre-fills out_pll_map[] with…
- [Low] sit9531x_parse_board_config() reads `sitime,pll-fvco` with a
  fixed count (device_property_read_u64_array(..., SIT9531X_NUM_PLLS) at…

Pre-existing issues:
- [Low] The kernel-doc-style header of sit9531x_get_fvco()
  (drivers/dpll/sit9531x/core.c:1488-1500) describes the function as…

--

dpll: sit9531x: allow the device tree to override two board facts

This adds sit9531x_parse_board_config(), which lets a board state its
per-PLL VCO frequency via sitime,pll-fvco and its output-to-PLL routing
via sitime,output-pll-map, both optional, plus a per-PLL band check on
the supplied Fvco values.  The parser is called from sit9531x_dev_probe()
just after the reset GPIO is taken.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index b15859890caa7..51f7d8a4ea445 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -3862,6 +3862,108 @@ static u64 sit9531x_derive_clock_id(struct sit9531x_dev *sitdev)
>  	return clkid;
>  }
>  
> +/*
> + * Does an Fvco fall in the band the given PLL runs in?
> + *
> + * The two bands are disjoint, and which one applies is fixed per PLL, so a
> + * single envelope from the bottom of the low band to the top of the high
> + * one would accept both the ~1 GHz gap between them and a rate belonging to
> + * the other PLL's band.
> + */
> +static bool sit9531x_fvco_in_band(u8 pll_idx, u64 fvco)
> +{
> +	if (pll_idx == 1 || pll_idx == 3)
> +		return fvco >= SIT9531X_FVCO_HIGHBAND_MIN &&
> +		       fvco <= SIT9531X_FVCO_HIGHBAND_MAX;
> +
> +	return fvco >= SIT9531X_FVCO_LOWBAND_MIN &&
> +	       fvco <= SIT9531X_FVCO_LOWBAND_MAX;
> +}
> +
> +/*
> + * 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, rc;
> +
> +	if (device_property_present(sitdev->dev, "sitime,pll-fvco")) {
> +		rc = device_property_read_u64_array(sitdev->dev,
> +						    "sitime,pll-fvco",
> +						    sitdev->pll_fvco,
> +						    SIT9531X_NUM_PLLS);

[Severity: Low]

Is an oversized sitime,pll-fvco silently truncated here?  This is a
fixed-count read and only the return value is checked, so for
device-tree firmware it reaches of_property_read_u64_array() with
sz_min = 4 and sz_max = 0, and of_find_property_value_of_size() only
rejects a too-long property when max is non-zero:

	if (max && prop->length > max)
		return ERR_PTR(-EOVERFLOW);

A property with five or more cells then returns success, the first four
are applied and the surplus is dropped without a diagnostic.  A short
array is correctly rejected with -EOVERFLOW, so only the long case goes
unreported.  Would device_property_count_u64() plus a count check be
more consistent with the map path below, which does validate the count,
and with the comment that mentions guarding against a wrong cell count?

[Severity: Low]

This isn't a bug introduced by this patch, but once pll_fvco[] can be
populated from DT, the header of sit9531x_get_fvco() no longer describes
what the function does.  It says:

 * 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.

while the first branch of the body returns the override without reading
DIVN at all:

	if (pll_idx < SIT9531X_NUM_PLLS && sitdev->pll_fvco[pll_idx]) {
		*fvco = sitdev->pll_fvco[pll_idx];
		return 0;
	}

Both the header and that branch predate this commit, which only adds the
parser that fills pll_fvco[].  Could the header mention the override
while the surrounding comments are being touched anyway?

> +		if (rc) {
> +			dev_warn(sitdev->dev,
> +				 "invalid sitime,pll-fvco (%d), ignoring\n",
> +				 rc);
> +			memset(sitdev->pll_fvco, 0, sizeof(sitdev->pll_fvco));
> +		}
> +
> +		/*
> +		 * The override is used verbatim by the divider math, so an
> +		 * implausible value (units typo, wrong cell count worked
> +		 * around with zeros) must not silently misprogram DIVO.
> +		 * Anything outside both VCO bands is dropped with a warning
> +		 * rather than trusted.
> +		 */
> +		for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
> +			u64 f = sitdev->pll_fvco[i];
> +
> +			if (f && !sit9531x_fvco_in_band(i, f)) {
> +				dev_warn(sitdev->dev,
> +					 "PLL%c Fvco override %llu Hz is outside the band that PLL runs in, ignoring\n",
> +					 'A' + i, f);
> +				sitdev->pll_fvco[i] = 0;
> +			}
> +		}
> +	}

[Severity: High]

Does this check reject the value in the binding's own example?
sit9531x_fvco_in_band() fixes PLLA and PLLC to the low band, whose top
is SIT9531X_FVCO_LOWBAND_MAX (5898240000), and
Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml has:

            sitime,pll-fvco = /bits/ 64 <6900000000 0 0 0>;

Index 0 is PLLA, so f = 6900000000 fails sit9531x_fvco_in_band(0, f) and
this loop sets pll_fvco[0] = 0 after only a dev_warn.  A device tree
copied from the documentation then gets the register-derived path
instead of the override.

Zeroing the entry does not mean "ignore the property" in the consumers,
it means "derive from DIVN".  sit9531x_get_fvco() falls through to
sit9531x_divn_static() and returns -ENODATA when DIVN is unprogrammed,
and sit9531x_output_divo_calc() turns that into -ENODEV:

	rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
	if (rc)
		return rc == -ENODATA ? -ENODEV : rc;
	if (!sitdev->pll_fvco[pll_idx]) {
		if (fvco < fvco_min)
			fvco = fvco_min;
		else if (fvco > fvco_max)
			fvco = fvco_max;
	}

So for the documented example the free-run case the commit message
describes either fails with -ENODEV or falls back to a DIVN-derived rate
clamped to at most 5.89824 GHz, and DIVO is then computed from
5.89824e9 rather than 6.9e9 (roughly 17 percent off) while the write is
reported as success.

The two consumer comments added earlier in the series state the opposite
intent.  sit9531x_get_fvco() says:

	 * 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.

and sit9531x_output_divo_calc() says the override is the source of truth
"e.g. a chip variant that runs out of the documented band" and therefore
skips the band clamp in that case, which after this patch can never
happen.

Should the band check warn but keep the board value, or should the
binding example, the per-index schema constraints and those two consumer
comments be corrected in the same patch?  As it stands the case the
property exists for is the one case that is dropped.

> +
> +	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)) {
> +		dev_warn(sitdev->dev,
> +			 "invalid sitime,output-pll-map, ignoring\n");
> +		return;
> +	}

[Severity: Medium]

Can a map shorter than the part's output count silently remove DPLL
output pins?  The only length test is n <= 0 || n > SIT9531X_MAX_OUTPUTS,
and out_pll_map_valid is then set unconditionally, so entries from n to
num_outputs - 1 stay at 0xFF and are trusted as real routing
information.

sit9531x_out_state_fetch() takes the unmapped branch and returns before
the OUT_MAP_LO/HI scan that would have found the routing:

	if (sitdev->out_pll_map_valid) {
		u8 m = sitdev->out_pll_map[index];

		if (m < SIT9531X_NUM_PLLS) { ... } else {
			out->pll_idx = 0;
			out->routed = false;
			out->enabled = false;
		}
		return 0;
	}

and sit9531x_dpll_pin_is_registrable() then refuses the pin:

	return sitdev->out[index].pll_idx == sitdpll->id &&
	       sitdev->out[index].routed;

So an accidentally truncated map (8, or even 1, entry on the 12-output
SiT95316) drops those pins with no diagnostic, while a bad per-entry
value below is rejected loudly.

The binding also states a different contract for the same property.
Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml declares
minItems: 8 / maxItems: 12, then maxItems: 8 for sitime,sit95317 and
minItems: 12 for sitime,sit95316, i.e. an exact length per variant,
while the comment above says any 1..MAX_OUTPUTS length is accepted.  In
the other direction a 12-entry map on a SiT95317 is accepted and entries
8..11 are ignored without comment.

Would it work to move the sit9531x_parse_board_config() call after
sit9531x_match_variant() and require n == info->num_outputs (or warn),
so the per-variant length the binding specifies can actually be checked?

> +
> +	/*
> +	 * The binding allows only 0-3 and 255 per entry.  A stray value
> +	 * would silently unroute an output (m >= SIT9531X_NUM_PLLS reads
> +	 * as unmapped in sit9531x_out_state_fetch()), so reject the whole
> +	 * property loudly instead.
> +	 */
> +	for (i = 0; i < n; i++) {
> +		if (map[i] >= SIT9531X_NUM_PLLS &&
> +		    map[i] != SIT9531X_OUT_PLL_UNMAPPED) {
> +			dev_warn(sitdev->dev,
> +				 "sitime,output-pll-map entry %d is %u (must be 0-3 or 255), ignoring map\n",
> +				 i, map[i]);
> +			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;
> @@ -3906,6 +4008,8 @@ int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
>  	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;

Nothing between this call and sit9531x_match_variant() appears to consume
the parsed data, so is there a reason the parse cannot run after
sitdev->info is known?

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260915000015.80480-1-arouhi%40sitime.com

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

end of thread, other threads:[~2026-09-17 15:02 UTC | newest]

Thread overview: 33+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-15  0:00 [PATCH net-next v9 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
2026-09-15  0:00 ` [PATCH net-next v9 01/15] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
2026-09-15  0:00 ` [PATCH net-next v9 03/15] dpll: add basic SiTime SiT9531x support Ali Rouhi
2026-09-17  8:42   ` Ivan Vecera
2026-09-17 15:01   ` netdev-bot+sashiko
2026-09-15  0:00 ` [PATCH net-next v9 02/15] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
2026-09-17 15:01   ` netdev-bot+sashiko
2026-09-15  0:00 ` [PATCH net-next v9 05/15] dpll: sit9531x: register DPLL devices and pins Ali Rouhi
2026-09-17 15:01   ` netdev-bot+sashiko
2026-09-15  0:00 ` [PATCH net-next v9 04/15] dpll: sit9531x: read DPLL types and pin properties from system firmware Ali Rouhi
2026-09-17  9:42   ` Ivan Vecera
2026-09-17 15:01   ` netdev-bot+sashiko
2026-09-15  0:00 ` [PATCH net-next v9 06/15] dpll: sit9531x: implement input pin state on a DPLL Ali Rouhi
2026-09-17 15:01   ` netdev-bot+sashiko
2026-09-15  0:00 ` [PATCH net-next v9 07/15] dpll: sit9531x: add support to get and set priority on input pins Ali Rouhi
2026-09-17 15:01   ` netdev-bot+sashiko
2026-09-15  0:00 ` [PATCH net-next v9 08/15] dpll: sit9531x: add support to get and set frequency on pins Ali Rouhi
2026-09-17 15:01   ` netdev-bot+sashiko
2026-09-15  0:00 ` [PATCH net-next v9 10/15] dpll: sit9531x: add support to adjust output phase Ali Rouhi
2026-09-17  9:55   ` Ivan Vecera
2026-09-17 15:01   ` netdev-bot+sashiko
2026-09-15  0:00 ` [PATCH net-next v9 09/15] dpll: sit9531x: implement output pin state on a DPLL Ali Rouhi
2026-09-17 15:01   ` netdev-bot+sashiko
2026-09-15  0:00 ` [PATCH net-next v9 12/15] dpll: sit9531x: add support to get phase offset on the connected input pin Ali Rouhi
2026-09-17 15:01   ` netdev-bot+sashiko
2026-09-15  0:00 ` [PATCH net-next v9 11/15] dpll: sit9531x: add support to get and set esync on pins Ali Rouhi
2026-09-17 15:01   ` netdev-bot+sashiko
2026-09-15  0:00 ` [PATCH net-next v9 13/15] dpll: sit9531x: add support to get fractional frequency offset Ali Rouhi
2026-09-17 15:01   ` netdev-bot+sashiko
2026-09-15  0:00 ` [PATCH net-next v9 14/15] dpll: sit9531x: model the inter-PLL sync net as a pair of pins Ali Rouhi
2026-09-17 15:01   ` netdev-bot+sashiko
2026-09-15  0:00 ` [PATCH net-next v9 15/15] dpll: sit9531x: allow the device tree to override two board facts Ali Rouhi
2026-09-17 15:02   ` netdev-bot+sashiko

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®