mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH net-next v8 01/15] dt-bindings: vendor-prefixes: add SiTime Corporation
  2026-09-02 21:40 [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
@ 2026-09-02 21:40 ` Ali Rouhi
  2026-09-02 21:40 ` [PATCH net-next v8 03/15] dpll: add basic SiTime SiT9531x support Ali Rouhi
                   ` (14 subsequent siblings)
  15 siblings, 0 replies; 31+ messages in thread
From: Ali Rouhi @ 2026-09-02 21:40 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, 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.43.0


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

* [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver
@ 2026-09-02 21:40 Ali Rouhi
  2026-09-02 21:40 ` [PATCH net-next v8 01/15] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
                   ` (15 more replies)
  0 siblings, 16 replies; 31+ messages in thread
From: Ali Rouhi @ 2026-09-02 21:40 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, 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/

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 introduces it feature by feature instead:

  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 was built on its own, with W=1 on
x86_64 and i386, and is sparse clean. Each one also links: no patch
calls something that a later patch introduces, so the module builds at
every step of the series and a bisect cannot land on a tree that fails
to compile.

The split makes no functional change of its own. The tree this series
ends at compiles to objects byte-identical to the tree the single patch
produced once the fixes below are applied to it.

Changes in v8:

  - Split the driver patch into the thirteen patches listed above.

  - Advertise automatic mode only. v7 mapped DPLL_MODE_MANUAL onto the
    chip's free-run bit and then refused DPLL_PIN_STATE_CONNECTED on
    input pins, so it offered the mode whose one purpose is manual
    selection and refused the only request that mode exists for.
    Free-run selects no input at all, so on this device it is a lock
    state, not a selection mode. v8 advertises AUTOMATIC only and
    refuses anything else from .mode_set(), which now clears the outer
    loop disable bit so a PLL left free-running by the loaded
    configuration returns to selecting from the priority table.
    Free-run is reported through lock status as UNLOCKED.

    The device can also pin one reference while the loop keeps running,
    so real MANUAL support is possible later. It is left out here
    because the mode mapping becomes ABI once merged, and it belongs in
    the same series as the userspace that needs it rather than being
    approximated now.

  - State the pin-state contract once, above the pin ops, and let the
    two selection-role pins share one implementation of it. The driver
    had five ops tables and five hand-written encodings of the same
    state machine, and every review round has found another place where
    two of them disagreed. This closes several findings at once:
    CONNECTED reported while the outer loop was disabled, membership
    taken from the shared-receiver refcount rather than from the
    priority table, SELECTABLE on an INTSYNC destination userspace had
    disconnected, .prio_set() inserting a source that was never in the
    table, and .prio_remove() returning 0 when the disconnect would
    have emptied it. The last two are now -EINVAL and -EBUSY.

  - Report LOCKED_HO_ACQ only once the holdover estimate is valid. It
    was asserted whenever the loop was locked, without reading the bit
    that says the device has accumulated a frequency estimate good
    enough to fall back on. That bit is distinct from the one that says
    the PLL has already switched over to holdover.

  - Fix the 32-bit build. The output phase adjust reduced a negative
    request modulo the output period with % on two u64 values, leaving
    the module with an undefined __umoddi3; it now uses div64_u64_rem(),
    which the rest of the arithmetic there already uses. Reported by
    Jakub Kicinski.

  - Correct the register model comment: the page selector is at 0xFF and
    a page holds 256 registers, as the definitions right below the
    comment already said. Reported by Ivan Vecera.

  - Type ordinary clock outputs as DPLL_PIN_TYPE_EXT instead of
    DPLL_PIN_TYPE_GNSS, which claimed a GNSS-disciplined timing port.
    Advertise a phase-adjust range on outputs only, and drop the input
    pins' .phase_adjust_get with it: inputs have no .phase_adjust_set,
    so a non-zero range promised something every set would refuse.

  - Always issue the loop lock in the programming commit, so a transient
    I2C error cannot leave the chip in PRG_CMD with its loops open. Undo
    the global enable when the INTSYNC enable fails partway. Take
    multiop_lock before testing the selected reference in the frequency
    offset getter, as the sibling phase-offset getter already does.

  - Latch every read of the chip's debug block the way the phase read
    does. That block returns a sample of an internal measurement tap,
    selected by a read code and latched by reading a trigger register.
    The generic helper read the trigger once where the phase path reads
    it three times, and a single read returns the previous latch. On
    hardware this showed as a frequency offset that would not move while
    a phase measurement of the same PLL tracked a temperature step
    cleanly.

  - Report which PLLs came up and which EEPROM profile was loaded, and
    warn when a PLL drives outputs without having reached its active
    state, or when the profile's stored and computed CRCs disagree. In
    both cases every register still reads back a plausible value, so
    nothing about the failure is visible until the measurements come out
    wrong.

  - Say what the phase offset getter measures, next to the callback: the
    loop's residual error sampled with the loop closed, which is the
    quantity the ABI asks for -- not the vendor's open-loop
    phase-difference procedure, which would need the digital loop filter
    frozen and would leave the PLL undisciplined until something
    released it. A netlink read must not do that.

  - Drop fifteen register definitions that have never had a caller.
    Eleven describe that open-loop sequence and the PLL restart path,
    neither of which the driver performs. The other four -- the PLL page
    macro and the three DIVN2 fields -- had no user in any patch of the
    split, so there was no honest patch to introduce them in. Each will
    come back with its first user.

  - Binding: put the example VCO frequency inside a band. 6251500000 sat
    in the gap between the device's two bands, which the property's own
    description rules out; it is now 6900000000, which a shipping
    profile on our SiT95316 board runs. This is the only change to the
    binding since v6 -- the property and its description are unchanged
    -- so Krzysztof's Reviewed-by is carried across it.

The AI review's 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.

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        |  171 +
 .../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                  | 3317 +++++++++++++++++
 drivers/dpll/sit9531x/core.h                  |  381 ++
 drivers/dpll/sit9531x/dpll.c                  | 1335 +++++++
 drivers/dpll/sit9531x/dpll.h                  |   69 +
 drivers/dpll/sit9531x/prop.c                  |  406 ++
 drivers/dpll/sit9531x/prop.h                  |   39 +
 drivers/dpll/sit9531x/regs.h                  |  377 ++
 14 files changed, 6128 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.43.0


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

* [PATCH net-next v8 03/15] dpll: add basic SiTime SiT9531x support
  2026-09-02 21:40 [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
  2026-09-02 21:40 ` [PATCH net-next v8 01/15] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
@ 2026-09-02 21:40 ` Ali Rouhi
  2026-09-08 12:45   ` netdev-bot+sashiko
  2026-09-02 21:40 ` [PATCH net-next v8 02/15] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
                   ` (13 subsequent siblings)
  15 siblings, 1 reply; 31+ messages in thread
From: Ali Rouhi @ 2026-09-02 21:40 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, 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   | 271 +++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/core.h   |  91 +++++++++++
 drivers/dpll/sit9531x/regs.h   |  60 ++++++++
 8 files changed, 447 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..f355b08aebca
--- /dev/null
+++ b/drivers/dpll/sit9531x/core.c
@@ -0,0 +1,271 @@
+// 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/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);
+}
+
+/*
+ * 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 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.
+	 */
+	xtal_clk = devm_clk_get_enabled(sitdev->dev, "xtal");
+	if (IS_ERR(xtal_clk))
+		return dev_err_probe(sitdev->dev, PTR_ERR(xtal_clk),
+				     "Failed to get xtal clock\n");
+	sitdev->xtal_freq = clk_get_rate(xtal_clk);
+	if (!sitdev->xtal_freq)
+		return dev_err_probe(sitdev->dev, -EINVAL,
+				     "xtal clock has no rate\n");
+
+	/*
+	 * Held deasserted, never pulsed: the chip configuration comes from
+	 * efuse or an NVM blob applied before probe, and a reset would
+	 * discard it.  Must precede the first I2C access, as a board that
+	 * powers up asserted keeps the chip unreachable until released.
+	 */
+	sitdev->reset_gpio = devm_gpiod_get_optional(sitdev->dev, "reset",
+						     GPIOD_OUT_LOW);
+	if (IS_ERR(sitdev->reset_gpio))
+		return dev_err_probe(sitdev->dev, PTR_ERR(sitdev->reset_gpio),
+				     "Failed to request reset gpio\n");
+	if (sitdev->reset_gpio)
+		fsleep(10000);	/* internal boot after release */
+
+	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..8337f1235472
--- /dev/null
+++ b/drivers/dpll/sit9531x/regs.h
@@ -0,0 +1,60 @@
+/* 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
+
+/* PLL index to page mapping */
+#define SIT9531X_PLL_PAGE(_idx) \
+	(SIT9531X_PAGE_PLLA + (_idx))
+
+/*
+ * VARIANT_ID is a single byte at page 0 reg 0x02 (95317 = 0x17, 95316 = 0x31).
+ * Reg 0x03 carries an unrelated revision byte and must not be combined into
+ * the variant identifier.
+ */
+#define SIT9531X_REG_VARIANT_ID		SIT9531X_REG(0x00, 0x02)
+
+/* Variant ID values (page 0 reg 0x02) */
+#define SIT9531X_VARIANT_ID_95317	0x17
+#define SIT9531X_VARIANT_ID_95316	0x31
+
+#endif /* _SIT9531X_REGS_H */
-- 
2.43.0


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

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

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


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

* [PATCH net-next v8 04/15] dpll: sit9531x: read DPLL types and pin properties from system firmware
  2026-09-02 21:40 [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (3 preceding siblings ...)
  2026-09-02 21:40 ` [PATCH net-next v8 05/15] dpll: sit9531x: register DPLL devices and pins Ali Rouhi
@ 2026-09-02 21:40 ` Ali Rouhi
  2026-09-08 12:45   ` netdev-bot+sashiko
  2026-09-02 21:40 ` [PATCH net-next v8 07/15] dpll: sit9531x: add support to get and set priority on input pins Ali Rouhi
                   ` (10 subsequent siblings)
  15 siblings, 1 reply; 31+ messages in thread
From: Ali Rouhi @ 2026-09-02 21:40 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, 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   | 406 +++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/prop.h   |  39 ++++
 4 files changed, 518 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..94f3da716297
--- /dev/null
+++ b/drivers/dpll/sit9531x/prop.c
@@ -0,0 +1,406 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * SiTime SiT9531x firmware node property parsing
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * Retrieves pin properties from Device Tree firmware nodes (or
+ * applies defaults when no firmware node exists).
+ */
+
+#include <linux/dev_printk.h>
+#include <linux/dpll.h>
+#include <linux/err.h>
+#include <linux/fwnode.h>
+#include <linux/property.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#include "core.h"
+#include "prop.h"
+
+/*
+ * sit9531x_input_pin_label - fill the package label for an input pin
+ *
+ * Split out so input-naming changes stay local to this helper.
+ */
+static void sit9531x_input_pin_label(struct sit9531x_dev *sitdev,
+				     struct sit9531x_pin_props *props, u8 id)
+{
+	u8 pair = sit9531x_input_pair(id);
+
+	if (sitdev->ref[id].sig_mode == SIT9531X_MODE_DE)
+		snprintf(props->package_label,
+			 sizeof(props->package_label), "IN%u", pair);
+	else
+		snprintf(props->package_label,
+			 sizeof(props->package_label), "IN%u%c", pair,
+			 sit9531x_input_is_n(id) ? 'N' : 'P');
+}
+
+/*
+ * sit9531x_prop_pin_package_label_set - generate package label
+ * @dir:	pin direction
+ * @id:		pin index
+ *
+ * Generates a package label string.  Output pins are named "OUT0",
+ * "OUT1", ...  Input pins are named after the physical pair and lane:
+ * "IN0P", "IN0N", "IN1P", ... for single-ended lanes, or "IN0",
+ * "IN1", ... when the pair is configured differential (the N lane is
+ * not registered in that case).
+ */
+static void
+sit9531x_prop_pin_package_label_set(struct sit9531x_dev *sitdev,
+				    struct sit9531x_pin_props *props,
+				    enum dpll_pin_direction dir, u8 id)
+{
+	/* The internal INTSYNC pin has a fixed label */
+	if (dir == DPLL_PIN_DIRECTION_INPUT &&
+	    id == SIT9531X_INTSYNC_PIN_ID) {
+		strscpy(props->package_label, "INTSYNC",
+			sizeof(props->package_label));
+		props->dpll_props.package_label = props->package_label;
+		return;
+	}
+
+	/* The internal XO reference has a fixed label */
+	if (dir == DPLL_PIN_DIRECTION_INPUT && id == SIT9531X_MAX_INPUTS) {
+		strscpy(props->package_label, "XO",
+			sizeof(props->package_label));
+		props->dpll_props.package_label = props->package_label;
+		return;
+	}
+
+	/* The internal INTSYNC source (output) pin has a fixed label */
+	if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+	    id == SIT9531X_INTSYNC_OUT_PIN_ID) {
+		strscpy(props->package_label, "SYNCOUT",
+			sizeof(props->package_label));
+		props->dpll_props.package_label = props->package_label;
+		return;
+	}
+
+	if (dir == DPLL_PIN_DIRECTION_INPUT)
+		sit9531x_input_pin_label(sitdev, props, id);
+	else
+		snprintf(props->package_label, sizeof(props->package_label),
+			 "OUT%u", id);
+
+	props->dpll_props.package_label = props->package_label;
+}
+
+/*
+ * sit9531x_prop_pin_fwnode_get - find firmware node for a pin
+ * @dir:	pin direction
+ * @id:		pin index
+ *
+ * Searches for input-pins/output-pins child nodes in DT, looking
+ * for a child whose "reg" property matches @id.
+ *
+ * Return: 0 on success, -ENOENT if no firmware node exists
+ */
+static int
+sit9531x_prop_pin_fwnode_get(struct sit9531x_dev *sitdev,
+			     struct sit9531x_pin_props *props,
+			     enum dpll_pin_direction dir, u8 id)
+{
+	struct fwnode_handle *pins_node, *pin_node;
+	const char *node_name;
+
+	if (dir == DPLL_PIN_DIRECTION_INPUT)
+		node_name = "input-pins";
+	else
+		node_name = "output-pins";
+
+	pins_node = device_get_named_child_node(sitdev->dev, node_name);
+	if (!pins_node) {
+		dev_dbg(sitdev->dev, "'%s' sub-node is missing\n", node_name);
+		return -ENOENT;
+	}
+
+	/* Enumerate child pin nodes and find the requested one */
+	fwnode_for_each_child_node(pins_node, pin_node) {
+		u32 reg;
+
+		if (fwnode_property_read_u32(pin_node, "reg", &reg))
+			continue;
+
+		if (id == reg)
+			break;
+	}
+
+	fwnode_handle_put(pins_node);
+
+	props->fwnode = pin_node;
+
+	dev_dbg(sitdev->dev, "Firmware node for %s %sfound\n",
+		props->package_label, pin_node ? "" : "NOT ");
+
+	return pin_node ? 0 : -ENOENT;
+}
+
+/*
+ * sit9531x_pin_props_get - get pin properties for a given pin
+ * @dir:	pin direction (INPUT or OUTPUT)
+ * @index:	pin index
+ *
+ * Allocates a pin properties structure, generates a package label,
+ * looks up the firmware node if available, and reads optional
+ * properties (label, connection-type, supported-frequencies-hz,
+ * esync-control).
+ *
+ * Call sit9531x_pin_props_put() to free the returned structure.
+ *
+ * Return: pointer to pin properties on success, error pointer on error
+ */
+struct sit9531x_pin_props *
+sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
+		       enum dpll_pin_direction dir, u8 index)
+{
+	struct dpll_pin_frequency *ranges;
+	struct sit9531x_pin_props *props;
+	int i, j, num_freqs = 0, rc;
+	u64 *freqs = NULL;
+	const char *type;
+	u32 curr_freq;
+
+	props = kzalloc_obj(*props, GFP_KERNEL);
+	if (!props)
+		return ERR_PTR(-ENOMEM);
+
+	if (dir == DPLL_PIN_DIRECTION_INPUT &&
+	    index == SIT9531X_INTSYNC_PIN_ID) {
+		/*
+		 * INTSYNC destination pin: a PLL locks to the INTSYNC net as a
+		 * reference, so it can be connected and re-prioritised.
+		 */
+		props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+		props->dpll_props.capabilities =
+			DPLL_PIN_CAPABILITIES_PRIORITY_CAN_CHANGE |
+			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+		curr_freq = 0;
+	} else if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+		   index == SIT9531X_INTSYNC_OUT_PIN_ID) {
+		/*
+		 * INTSYNC source pin: a PLL drives the INTSYNC net.  It can be
+		 * connected/disconnected but carries no priority (driving the
+		 * net is not a reference selection) and no frequency.
+		 */
+		props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+		props->dpll_props.capabilities =
+			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+		curr_freq = 0;
+	} else if (dir == DPLL_PIN_DIRECTION_INPUT &&
+		   index == SIT9531X_MAX_INPUTS) {
+		/* The XO reference is fixed: no state or priority control. */
+		props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+		props->dpll_props.capabilities = 0;
+		sitdev->ref[index].freq = sitdev->xtal_freq;
+		curr_freq = sitdev->xtal_freq;
+	} else if (dir == DPLL_PIN_DIRECTION_INPUT) {
+		props->dpll_props.type = DPLL_PIN_TYPE_EXT;
+		props->dpll_props.capabilities =
+			DPLL_PIN_CAPABILITIES_PRIORITY_CAN_CHANGE |
+			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+		curr_freq = sitdev->ref[index].freq;
+	} else {
+		/*
+		 * 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 */
+	}
+
+	/* Generate package label */
+	sit9531x_prop_pin_package_label_set(sitdev, props, dir, index);
+
+	/*
+	 * Both INTSYNC pins are internal to the chip and have no board-level
+	 * wiring, so they take no properties from the firmware node.
+	 */
+	if (dir == DPLL_PIN_DIRECTION_INPUT &&
+	    index == SIT9531X_INTSYNC_PIN_ID)
+		goto skip_fwnode_props;
+	if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+	    index == SIT9531X_INTSYNC_OUT_PIN_ID)
+		goto skip_fwnode_props;
+
+	rc = sit9531x_prop_pin_fwnode_get(sitdev, props, dir, index);
+	if (rc)
+		goto skip_fwnode_props;
+
+	/* Look for "label" property -> board label */
+	fwnode_property_read_string(props->fwnode, "label",
+				    &props->dpll_props.board_label);
+
+	/* Look for "connection-type" property -> pin type enum */
+	if (!fwnode_property_read_string(props->fwnode, "connection-type",
+					 &type)) {
+		if (!strcmp(type, "ext"))
+			props->dpll_props.type = DPLL_PIN_TYPE_EXT;
+		else if (!strcmp(type, "gnss"))
+			props->dpll_props.type = DPLL_PIN_TYPE_GNSS;
+		else if (!strcmp(type, "int") ||
+			 !strcmp(type, "int-oscillator"))
+			props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+		else if (!strcmp(type, "synce") ||
+			 !strcmp(type, "synce-eth-port"))
+			props->dpll_props.type = DPLL_PIN_TYPE_SYNCE_ETH_PORT;
+		else if (!strcmp(type, "mux"))
+			props->dpll_props.type = DPLL_PIN_TYPE_MUX;
+		else
+			dev_warn(sitdev->dev,
+				 "Unknown pin type '%s'\n", type);
+	}
+
+	props->esync_control =
+		fwnode_property_read_bool(props->fwnode, "esync-control");
+
+	num_freqs = fwnode_property_count_u64(props->fwnode,
+					      "supported-frequencies-hz");
+	if (num_freqs <= 0) {
+		num_freqs = 0;
+		goto skip_fwnode_props;
+	}
+
+	freqs = kcalloc(num_freqs, sizeof(*freqs), GFP_KERNEL);
+	if (!freqs) {
+		rc = -ENOMEM;
+		goto err_alloc_freqs;
+	}
+
+	fwnode_property_read_u64_array(props->fwnode,
+				       "supported-frequencies-hz",
+				       freqs, num_freqs);
+
+	/*
+	 * Seed the runtime ref->freq / out->freq with the first DT-listed
+	 * supported frequency so the netlink frequency_get callback reports
+	 * a sane initial value before any pin_set occurs.  DT lists the
+	 * physically-wired reference frequency for each input pin and the
+	 * default output frequency for each output pin.
+	 */
+	if (num_freqs > 0) {
+		if (dir == DPLL_PIN_DIRECTION_INPUT)
+			sitdev->ref[index].freq = (u32)freqs[0];
+		else
+			sitdev->out[index].freq = (u32)freqs[0];
+		curr_freq = (u32)freqs[0];
+	}
+
+skip_fwnode_props:
+	/* Neither INTSYNC pin carries a frequency attribute */
+	if (dir == DPLL_PIN_DIRECTION_INPUT &&
+	    index == SIT9531X_INTSYNC_PIN_ID)
+		return props;
+	if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+	    index == SIT9531X_INTSYNC_OUT_PIN_ID)
+		return props;
+
+	/* Allocate frequency ranges list -- DT discrete entries + current
+	 * freq + one catch-all wide range so the subsystem never pre-
+	 * rejects a frequency_set call.  The chip's real admissible set
+	 * is bounded by VCO / divider math in sit9531x_output_freq_set().
+	 */
+	ranges = kcalloc(num_freqs + 2, sizeof(*ranges), GFP_KERNEL);
+	if (!ranges) {
+		rc = -ENOMEM;
+		goto err_alloc_ranges;
+	}
+
+	/* Current freq as first entry */
+	ranges[0] = (struct dpll_pin_frequency)DPLL_PIN_FREQUENCY(curr_freq);
+	j = 1;
+
+	for (i = 0; i < num_freqs; i++) {
+		struct dpll_pin_frequency freq = DPLL_PIN_FREQUENCY(freqs[i]);
+
+		if (freqs[i] == curr_freq)
+			continue;
+		ranges[j++] = freq;
+	}
+
+	/* Always append a wide catch-all range */
+	ranges[j].min = 1;
+	ranges[j].max = 1000000000ULL; /* 1 GHz */
+	j++;
+
+	props->dpll_props.freq_supported = ranges;
+	props->dpll_props.freq_supported_num = j;
+
+	kfree(freqs);
+
+	return props;
+
+err_alloc_ranges:
+	kfree(freqs);
+err_alloc_freqs:
+	fwnode_handle_put(props->fwnode);
+	kfree(props);
+
+	return ERR_PTR(rc);
+}
+
+/*
+ * sit9531x_pin_props_put - release pin properties
+ * @props:	pin properties to free
+ */
+void sit9531x_pin_props_put(struct sit9531x_pin_props *props)
+{
+	kfree(props->dpll_props.freq_supported);
+
+	if (props->fwnode)
+		fwnode_handle_put(props->fwnode);
+
+	kfree(props);
+}
+
+/*
+ * sit9531x_prop_dpll_type_get - get DPLL channel type from firmware
+ * @index:	DPLL channel index (0-3)
+ *
+ * Reads the "dpll-types" string array property from the firmware node
+ * and returns the corresponding DPLL type enum.
+ *
+ * Return: DPLL type for the given channel (default: DPLL_TYPE_PPS)
+ */
+enum dpll_type
+sit9531x_prop_dpll_type_get(struct sit9531x_dev *sitdev, u8 index)
+{
+	const char *types[SIT9531X_NUM_PLLS];
+	int count;
+
+	count = device_property_read_string_array(sitdev->dev, "dpll-types",
+						  types, ARRAY_SIZE(types));
+
+	if (index >= count)
+		return DPLL_TYPE_PPS;
+
+	if (!strcmp(types[index], "pps"))
+		return DPLL_TYPE_PPS;
+	else if (!strcmp(types[index], "eec"))
+		return DPLL_TYPE_EEC;
+
+	dev_warn(sitdev->dev, "Unknown DPLL type '%s', using default\n",
+		 types[index]);
+
+	return DPLL_TYPE_PPS;
+}
diff --git a/drivers/dpll/sit9531x/prop.h b/drivers/dpll/sit9531x/prop.h
new file mode 100644
index 000000000000..f7f1c854b955
--- /dev/null
+++ b/drivers/dpll/sit9531x/prop.h
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * SiTime SiT9531x firmware node property parsing
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ */
+
+#ifndef _SIT9531X_PROP_H
+#define _SIT9531X_PROP_H
+
+#include <linux/dpll.h>
+#include <linux/fwnode.h>
+
+struct sit9531x_dev;
+
+/*
+ * struct sit9531x_pin_props - pin properties from firmware
+ * @fwnode:		firmware node handle (NULL if no DT node)
+ * @dpll_props:		DPLL core pin properties
+ * @package_label:	pin package label (e.g. "IN0", "OUT3")
+ * @esync_control:	embedded sync is controllable
+ */
+struct sit9531x_pin_props {
+	struct fwnode_handle		*fwnode;
+	struct dpll_pin_properties	dpll_props;
+	char				package_label[8];
+	bool				esync_control;
+};
+
+enum dpll_type sit9531x_prop_dpll_type_get(struct sit9531x_dev *sitdev,
+					   u8 index);
+struct sit9531x_pin_props *sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
+						  enum dpll_pin_direction dir,
+						  u8 index);
+void sit9531x_pin_props_put(struct sit9531x_pin_props *props);
+
+#endif /* _SIT9531X_PROP_H */
-- 
2.43.0


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

* [PATCH net-next v8 05/15] dpll: sit9531x: register DPLL devices and pins
  2026-09-02 21:40 [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (2 preceding siblings ...)
  2026-09-02 21:40 ` [PATCH net-next v8 02/15] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
@ 2026-09-02 21:40 ` Ali Rouhi
  2026-09-08 12:45   ` netdev-bot+sashiko
  2026-09-02 21:40 ` [PATCH net-next v8 04/15] dpll: sit9531x: read DPLL types and pin properties from system firmware Ali Rouhi
                   ` (11 subsequent siblings)
  15 siblings, 1 reply; 31+ messages in thread
From: Ali Rouhi @ 2026-09-02 21:40 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, 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.

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   | 1517 +++++++++++++++++++++++++++++++-
 drivers/dpll/sit9531x/core.h   |  264 +++++-
 drivers/dpll/sit9531x/dpll.c   |  417 +++++++++
 drivers/dpll/sit9531x/dpll.h   |   69 ++
 drivers/dpll/sit9531x/regs.h   |  223 ++++-
 6 files changed, 2417 insertions(+), 75 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 f355b08aebca..1394ea1077d9 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,13 +18,25 @@
 #include <linux/device.h>
 #include <linux/gpio/consumer.h>
 #include <linux/i2c.h>
+#include <linux/interrupt.h>
 #include <linux/kernel.h>
+#include <linux/kthread.h>
 #include <linux/module.h>
+#include <linux/property.h>
 #include <linux/regmap.h>
+#include <linux/slab.h>
+#include <linux/string.h>
 
 #include "core.h"
+#include "dpll.h"
+#include "prop.h"
 #include "regs.h"
 
+/* Number of input + output pin positions for pin index allocation */
+#define SIT9531X_NUM_INPUT_PINS		(SIT9531X_MAX_INPUTS + 2) /* +xtal +INTSYNC */
+#define SIT9531X_NUM_OUTPUT_PINS	(SIT9531X_MAX_OUTPUTS + 1) /* +INTSYNC src */
+#define SIT9531X_NUM_PINS_TOTAL		(SIT9531X_NUM_INPUT_PINS + SIT9531X_NUM_OUTPUT_PINS)
+
 #define SIT9531X_CHIP(_id, _nin, _nout, _name, _map) \
 	{ .id = (_id), .num_inputs = (_nin), .num_outputs = (_nout), \
 	  .name = (_name), .clkout_map = (_map) }
@@ -153,76 +167,1487 @@ int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
  * Selects the correct Page 0x02 register pair based on the pair's
  * signal mode and the lane (P/N) the index refers to.
  */
+static void sit9531x_input_get_regs(const struct sit9531x_dev *sitdev,
+				    u8 index,
+				    unsigned int *force_reg,
+				    unsigned int *state_reg)
+{
+	if (sitdev->ref[index].sig_mode == SIT9531X_MODE_DE) {
+		*force_reg = SIT9531X_REG_IN_DE_FORCE;
+		*state_reg = SIT9531X_REG_IN_DE_STATE;
+	} else if (sit9531x_input_is_n(index)) {
+		*force_reg = SIT9531X_REG_IN_SEN_FORCE;
+		*state_reg = SIT9531X_REG_IN_SEN_STATE;
+	} else {
+		*force_reg = SIT9531X_REG_IN_SEP_FORCE;
+		*state_reg = SIT9531X_REG_IN_SEP_STATE;
+	}
+}
 
-static int sit9531x_read_variant_id(struct sit9531x_dev *sitdev, u8 *id)
+/*
+ * Output enable / disable (Hi-Z control)
+ *
+ * SiT9531x outputs can be configured as differential (DIFF) or
+ * single-ended (SE) depending on the factory blob.  Each output slot
+ * has TWO Hi-Z force/state register pairs on Page 0x03 -- one for the
+ * DIFF path, one for the SE path.
+ *
+ * We write to BOTH pairs so the function mutes the output regardless
+ * of whether it's been configured DIFF or SE on this board.
+ *
+ *   slot 0-7 :
+ *     DIFF mask=0xF2 state=0xF3   SE mask=0xF8 state=0xF9
+ *   slot 8-11:
+ *     DIFF mask=0xF4 state=0xF5   SE mask=0xFA state=0xFB
+ *
+ * MASK bit = 1  -> driver takes control of that output's Hi-Z state
+ * STATE bit = 0 -> output is forced to Hi-Z (muted)
+ * STATE bit = 1 -> output is driven (active)
+ *
+ * The output "index" in the driver is logical; the physical slot comes
+ * from info->clkout_map[].
+ */
+
+struct sit9531x_hiz_regs {
+	unsigned int diff_mask;
+	unsigned int diff_state;
+	unsigned int se_mask;
+	unsigned int se_state;
+	u8 bit;
+};
+
+static void sit9531x_output_get_hiz_regs(u8 slot,
+					 struct sit9531x_hiz_regs *r)
 {
-	return sit9531x_read_u8(sitdev, SIT9531X_REG_VARIANT_ID, id);
+	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;
+	}
 }
 
-static const struct sit9531x_chip_info *sit9531x_match_variant(u8 id)
+/*
+ * Report whether a slot is currently forced into Hi-Z, i.e. the driver
+ * (or the blob) took control of the Hi-Z state (MASK bit set) and drives
+ * it low (STATE bit clear).  Either register pair muting the slot counts,
+ * mirroring what sit9531x_output_disable() programs.
+ */
+static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev, u8 slot,
+				      bool *muted)
 {
-	unsigned int i;
+	struct sit9531x_hiz_regs r;
+	u8 mask, state;
+	int rc;
 
-	for (i = 0; i < ARRAY_SIZE(sit9531x_chip_ids); i++) {
-		if (sit9531x_chip_ids[i].id == id)
-			return &sit9531x_chip_ids[i];
+	sit9531x_output_get_hiz_regs(slot, &r);
+
+	rc = sit9531x_read_u8(sitdev, r.diff_mask, &mask);
+	if (rc)
+		return rc;
+	rc = sit9531x_read_u8(sitdev, r.diff_state, &state);
+	if (rc)
+		return rc;
+
+	*muted = (mask & BIT(r.bit)) && !(state & BIT(r.bit));
+	if (*muted)
+		return 0;
+
+	rc = sit9531x_read_u8(sitdev, r.se_mask, &mask);
+	if (rc)
+		return rc;
+	rc = sit9531x_read_u8(sitdev, r.se_state, &state);
+	if (rc)
+		return rc;
+
+	*muted = (mask & BIT(r.bit)) && !(state & BIT(r.bit));
+
+	return 0;
+}
+
+/*
+ * 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);
 	}
 
-	return NULL;
+	return 0;
 }
 
-int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
+/* 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
+
+/*
+ * 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)
 {
-	struct clk *xtal_clk;
-	u8 variant_id;
+	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;
 
 	/*
-	 * 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 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.
 	 */
-	xtal_clk = devm_clk_get_enabled(sitdev->dev, "xtal");
-	if (IS_ERR(xtal_clk))
-		return dev_err_probe(sitdev->dev, PTR_ERR(xtal_clk),
-				     "Failed to get xtal clock\n");
-	sitdev->xtal_freq = clk_get_rate(xtal_clk);
-	if (!sitdev->xtal_freq)
-		return dev_err_probe(sitdev->dev, -EINVAL,
-				     "xtal clock has no rate\n");
+	if (index >= SIT9531X_MAX_INPUTS)
+		return -EINVAL;
+
+	ref = &sitdev->ref[index];
+	pair = sit9531x_input_pair(index);
+
+	if (sit9531x_input_is_n(index))
+		reg = pair < 2 ? SIT9531X_CLKMON_N_STATUS_01
+			       : SIT9531X_CLKMON_N_STATUS_23;
+	else
+		reg = pair < 2 ? SIT9531X_CLKMON_P_STATUS_01
+			       : SIT9531X_CLKMON_P_STATUS_23;
+
+	rc = sit9531x_read_u8(sitdev, reg, &status);
+	if (rc)
+		return rc;
+
+	if (pair & 1)
+		status >>= 4;
+
+	ref->los = !!(status & (BIT(SIT9531X_CLKMON_CLK_LOSS) |
+				BIT(SIT9531X_CLKMON_CLK_LOSS_FD)));
+	ref->oof = !!(status & (BIT(SIT9531X_CLKMON_FREQ_FINE) |
+				BIT(SIT9531X_CLKMON_FREQ_COARSE)));
 
 	/*
-	 * 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.
+	 * 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.
 	 */
-	sitdev->reset_gpio = devm_gpiod_get_optional(sitdev->dev, "reset",
-						     GPIOD_OUT_LOW);
-	if (IS_ERR(sitdev->reset_gpio))
-		return dev_err_probe(sitdev->dev, PTR_ERR(sitdev->reset_gpio),
-				     "Failed to request reset gpio\n");
-	if (sitdev->reset_gpio)
-		fsleep(10000);	/* internal boot after release */
+	sit9531x_input_get_regs(sitdev, index, &force_reg, &state_reg);
 
-	rc = sit9531x_read_variant_id(sitdev, &variant_id);
+	rc = sit9531x_read_u8(sitdev, force_reg, &force);
+	if (rc)
+		return rc;
+	rc = sit9531x_read_u8(sitdev, state_reg, &state);
 	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);
+	ref->enabled = !((force & BIT(pair)) && !(state & BIT(pair)));
 
-	rc = devm_mutex_init(sitdev->dev, &sitdev->multiop_lock);
+	return 0;
+}
+
+/*
+ * sit9531x_input_mode_fetch - detect SE/DE configuration of an input pair
+ * @pair:	input pair number (0-3)
+ *
+ * Reads CLKINx_INPUT_MODE and stores the detected signal mode on both
+ * lanes of the pair.  A pair with neither SE lane enabled is running
+ * differential.
+ */
+static int sit9531x_input_mode_fetch(struct sit9531x_dev *sitdev, u8 pair)
+{
+	enum sit9531x_signal_mode sig_mode;
+	u8 mode;
+	int rc;
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_IN_MODE(pair), &mode);
 	if (rc)
-		return dev_err_probe(sitdev->dev, rc,
-				     "Failed to initialize mutex\n");
+		return rc;
+
+	if (mode & (SIT9531X_IN_MODE_SE_P_EN | SIT9531X_IN_MODE_SE_N_EN))
+		sig_mode = SIT9531X_MODE_SE;
+	else
+		sig_mode = SIT9531X_MODE_DE;
+
+	sitdev->ref[pair * 2].sig_mode = sig_mode;
+	sitdev->ref[pair * 2 + 1].sig_mode = sig_mode;
+
+	dev_dbg(sitdev->dev, "CLKIN%u mode reg 0x%02x -> %s\n", pair, mode,
+		sig_mode == SIT9531X_MODE_DE ? "differential" : "single-ended");
+
+	return 0;
+}
+
+/*
+ * sit9531x_chan_state_fetch - read PLL channel status from hardware
+ *
+ * Reads lock status and mode from the PLL status register.
+ */
+/* Read the PLL active-state bit (PLL page reg 0x02 bit 0). */
+static int sit9531x_pll_is_active(struct sit9531x_dev *sitdev, u8 pll_idx,
+				  bool *active)
+{
+	u8 v;
+	int rc;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_ACTIVE, &v);
+	if (rc)
+		return rc;
+
+	*active = !!(v & SIT9531X_PLL_ACTIVE_BIT);
+
+	return 0;
+}
+
+static int sit9531x_chan_state_fetch(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+	u8 status, outer_lol, input_sel, inner_lol, ho_freeze, activesel_reg;
+	struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+	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_forced_hiz(sitdev, slot, &muted);
+	if (rc)
+		return rc;
+
+	/*
+	 * DT board-config override: the per-PLL OUTPUT_ENABLE bitmaps
+	 * (0x27/0x28) do not unambiguously express output->PLL routing on
+	 * every config (overlaps, and some outputs routed outside that
+	 * path).  When the board supplies an explicit map, trust it.
+	 */
+	if (sitdev->out_pll_map_valid) {
+		u8 m = sitdev->out_pll_map[index];
+
+		if (m < SIT9531X_NUM_PLLS) {
+			out->pll_idx = m;
+			out->routed = true;
+			out->enabled = !muted;
+		} else {
+			out->pll_idx = 0;
+			out->routed = false;
+			out->enabled = false;
+		}
+		return 0;
+	}
+
+	/*
+	 * The OUT_MAP_LO/HI bitmaps are indexed by the physical slot the
+	 * output occupies on the chip, not by the driver's logical output
+	 * index (translated above via the chip-info clkout_map[]: identity
+	 * on SiT95316, non-contiguous on SiT95317).
+	 *
+	 * Determine which PLL drives this output by checking each PLL's
+	 * output map registers (0x27 = slots 8-11, 0x28 = slots 0-7).
+	 */
+	for (pll_idx = 0; pll_idx < SIT9531X_NUM_PLLS; pll_idx++) {
+		rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+					  SIT9531X_PLL_REG_OUT_MAP_LO, &map_lo);
+		if (rc)
+			return rc;
+
+		rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+					  SIT9531X_PLL_REG_OUT_MAP_HI, &map_hi);
+		if (rc)
+			return rc;
+
+		if (slot < 8) {
+			if (map_lo & BIT(slot)) {
+				out->pll_idx = pll_idx;
+				out->routed = true;
+				out->enabled = !muted;
+				return 0;
+			}
+		} else {
+			if (map_hi & BIT(slot - 8)) {
+				out->pll_idx = pll_idx;
+				out->routed = true;
+				out->enabled = !muted;
+				return 0;
+			}
+		}
+	}
+
+	/* Output not mapped to any PLL */
+	out->pll_idx = 0;
+	out->routed = false;
+	out->enabled = false;
+
+	return 0;
+}
+
+/*
+ * sit9531x_ref_pll_mask_fetch - seed the input-to-PLL usage masks
+ *
+ * ref->pll_mask is the refcount the disconnect path uses to decide when
+ * an input receiver may be powered down: the physical input is only
+ * disabled once the last DPLL has released it.  It therefore has to
+ * start out matching the hardware.  Without this pass every mask starts
+ * at zero, and disconnecting an input from one DPLL drops the mask to
+ * zero and disables a receiver the other DPLLs are still locked to.
+ *
+ * An input is counted for a PLL when it appears in that PLL's Page-1
+ * priority table, which is exactly the condition the connect and
+ * disconnect callbacks maintain.  Sources that are not physical inputs
+ * (OCXO, INTSYNC) and reserved codes are skipped.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_ref_pll_mask_fetch(struct sit9531x_dev *sitdev)
+{
+	u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+	u8 pll_idx, slot, index;
+	int rc;
+
+	for (pll_idx = 0; pll_idx < SIT9531X_NUM_PLLS; pll_idx++) {
+		rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+		if (rc)
+			return rc;
+
+		for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+			index = sit9531x_hw_src_input(srcs[slot]);
+			if (index >= sitdev->info->num_inputs)
+				continue;
+
+			/*
+			 * On a differentially configured pair only the P lane
+			 * has a DPLL pin, so that is the lane the connect and
+			 * disconnect callbacks account for.  Fold an N-lane
+			 * table entry onto its P lane, otherwise the count
+			 * would land on a lane nothing ever releases.  The
+			 * 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--;
 
-	dev_info(sitdev->dev, "%s detected, %u inputs, %u outputs\n",
-		 sitdev->info->name, sitdev->info->num_inputs,
-		 sitdev->info->num_outputs);
+			sitdev->ref[index].pll_mask |= BIT(pll_idx);
+		}
+	}
+
+	return 0;
+}
+
+/*
+ * sit9531x_dev_state_fetch - read all hardware state at startup
+ *
+ * Called once during probe to populate the initial state cache.
+ */
+static int sit9531x_dev_state_fetch(struct sit9531x_dev *sitdev)
+{
+	int rc;
+	u8 i;
+
+	/* Detect SE/DE configuration before any per-lane access */
+	for (i = 0; i < sitdev->info->num_inputs / 2; i++) {
+		rc = sit9531x_input_mode_fetch(sitdev, i);
+		if (rc) {
+			dev_err(sitdev->dev,
+				"Failed to fetch CLKIN%u mode: %d\n", i, rc);
+			return rc;
+		}
+	}
+
+	for (i = 0; i < sitdev->info->num_inputs; i++) {
+		rc = sit9531x_ref_state_fetch(sitdev, i);
+		if (rc) {
+			dev_err(sitdev->dev,
+				"Failed to fetch input %u state: %d\n", i, rc);
+			return rc;
+		}
+	}
+
+	/*
+	 * The priority-table read walks the Page-1 registers, so it runs
+	 * with multiop_lock held like every other multi-register sequence.
+	 * Nothing can race with it here -- the DPLLs are not registered and
+	 * the monitor is not running yet -- but the page handling stays
+	 * serialized the same way as at runtime.
+	 */
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_ref_pll_mask_fetch(sitdev);
+	mutex_unlock(&sitdev->multiop_lock);
+	if (rc) {
+		dev_err(sitdev->dev,
+			"Failed to fetch input priority tables: %d\n", rc);
+		return rc;
+	}
+
+	for (i = 0; i < sitdev->info->num_outputs; i++) {
+		rc = sit9531x_out_state_fetch(sitdev, i);
+		if (rc) {
+			dev_err(sitdev->dev,
+				"Failed to fetch output %u state: %d\n", i, rc);
+			return rc;
+		}
+	}
+
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		rc = sit9531x_chan_state_fetch(sitdev, i);
+		if (rc) {
+			dev_err(sitdev->dev,
+				"Failed to fetch PLL%c state: %d\n",
+				'A' + i, rc);
+			return rc;
+		}
+	}
+
+	return 0;
+}
+
+static void sit9531x_dev_ref_states_update(struct sit9531x_dev *sitdev)
+{
+	int i, rc;
+
+	for (i = 0; i < sitdev->info->num_inputs; i++) {
+		rc = sit9531x_ref_state_fetch(sitdev, i);
+		if (rc)
+			dev_warn(sitdev->dev,
+				 "Failed to get REF%u status: %d\n", i, rc);
+	}
+}
+
+static void sit9531x_dev_chan_states_update(struct sit9531x_dev *sitdev)
+{
+	int i, rc;
+
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		rc = sit9531x_chan_state_fetch(sitdev, i);
+		if (rc)
+			dev_warn(sitdev->dev,
+				 "Failed to get PLL%c state: %d\n",
+				 'A' + i, rc);
+	}
+}
+
+/*
+ * sit9531x_dev_periodic_work - periodic hardware state polling
+ * @work:	kthread_work pointer
+ *
+ * Polls hardware state at SIT9531X_STATUS_POLL_MS intervals.
+ * Updates reference and channel states, then delegates change
+ * detection to sit9531x_dpll_changes_check() for each registered DPLL.
+ */
+static void sit9531x_dev_periodic_work(struct kthread_work *work)
+{
+	struct sit9531x_dev *sitdev = container_of(work, struct sit9531x_dev,
+						   work.work);
+	struct sit9531x_dpll *sitdpll;
+	int rc;
+
+	/*
+	 * Update the cached ref[]/chan[] arrays under multiop_lock so the
+	 * fetches are serialized against the DPLL callbacks that read
+	 * these fields and against the chip's page selector.
+	 *
+	 * The lock is then dropped before sit9531x_dpll_changes_check(),
+	 * which calls dpll_pin_change_ntf() / dpll_device_change_ntf().
+	 * Those notification helpers take DPLL-subsystem locks that are
+	 * already held when our callbacks are invoked from netlink
+	 * context, and nesting multiop_lock around them would invert the
+	 * lock order.  changes_check() reads the cache published above,
+	 * which is already consistent.
+	 */
+	mutex_lock(&sitdev->multiop_lock);
+	sit9531x_dev_ref_states_update(sitdev);
+	sit9531x_dev_chan_states_update(sitdev);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	list_for_each_entry(sitdpll, &sitdev->dplls, list)
+		sit9531x_dpll_changes_check(sitdpll);
+
+	/*
+	 * Acknowledge the chip's notification latches after the tick has
+	 * read and acted on them.  Without this, the W1C bits remain set
+	 * and -- on boards that wire INTRB -- the line stays asserted,
+	 * re-firing the threaded handler back to back.  The helper writes
+	 * W1C bits across page 0 and page 6 and must run under
+	 * multiop_lock to serialize the page selector against userspace
+	 * dpll ops.  Failure is non-fatal: status was already consumed
+	 * for this tick and the next tick re-processes whatever stayed
+	 * latched.
+	 */
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_clear_notifications(sitdev);
+	mutex_unlock(&sitdev->multiop_lock);
+	if (rc)
+		dev_warn_ratelimited(sitdev->dev,
+				     "Failed to clear notifications: %d\n",
+				     rc);
+
+	/* Run twice a second */
+	kthread_queue_delayed_work(sitdev->kworker, &sitdev->work,
+				   msecs_to_jiffies(SIT9531X_STATUS_POLL_MS));
+}
+
+/*
+ * sit9531x_irq_thread_fn - threaded IRQ handler for the chip's INTRB line
+ *
+ * Triggered when the chip asserts INTRB (and only when DT wires up the
+ * client interrupt; absent property == handler never installed).  The
+ * action mirrors a periodic-work tick: queue an immediate run so status
+ * registers are read and DPLL changes_check fires without waiting for
+ * the next poll deadline.  Polling continues to run as a fallback.
+ */
+static irqreturn_t sit9531x_irq_thread_fn(int irq, void *data)
+{
+	struct sit9531x_dev *sitdev = data;
+	int rc;
+
+	/*
+	 * Acknowledge the chip's notification latches from the threaded
+	 * handler itself.  With IRQF_ONESHOT the line is unmasked on
+	 * return, so deferring the W1C clear to the async kworker would
+	 * let a still-asserted INTRB re-fire immediately (interrupt storm).
+	 * Clear here, then kick the poll worker to read state and run
+	 * changes_check.
+	 */
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_clear_notifications(sitdev);
+	mutex_unlock(&sitdev->multiop_lock);
+	if (rc)
+		dev_warn_ratelimited(sitdev->dev,
+				     "IRQ: failed to clear notifications: %d\n",
+				     rc);
+
+	kthread_mod_delayed_work(sitdev->kworker, &sitdev->work, 0);
+	return IRQ_HANDLED;
+}
+
+/*
+ * sit9531x_dev_start - start normal operation
+ *
+ * Fetches initial hardware state, registers all DPLL devices and
+ * their pins, and starts the periodic monitoring thread.
+ */
+/*
+ * 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.  A zero calculated CRC means nothing was read at all.
+	 */
+	if (rec_crc != cal_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_input_pin_is_registrable - check if an input pin is registrable
+ *
+ * Split out so input-model changes stay local to this helper.
+ *
+ * Return: true if the input pin should be registered, false otherwise
+ */
+static bool sit9531x_input_pin_is_registrable(struct sit9531x_dev *sitdev,
+					      u8 index)
+{
+	if (index >= sitdev->info->num_inputs)
+		return false;
+
+	/*
+	 * The N lane of a differentially-configured pair is not a
+	 * standalone input and is skipped (zl3073x model).
+	 */
+	if (sit9531x_input_is_n(index) &&
+	    sitdev->ref[index].sig_mode == SIT9531X_MODE_DE)
+		return false;
+
+	return true;
+}
+
+/*
+ * sit9531x_dpll_pin_is_registrable - check if a pin should be registered
+ * @dir:	pin direction
+ * @index:	pin hardware index
+ *
+ * For input pins: delegate to sit9531x_input_pin_is_registrable().
+ * For output pins: the pin is registrable if this DPLL is routed to it,
+ * whether or not it is currently driving.
+ *
+ * Return: true if pin should be registered, false otherwise
+ */
+static bool sit9531x_dpll_pin_is_registrable(struct sit9531x_dpll *sitdpll,
+					     enum dpll_pin_direction dir,
+					     u8 index)
+{
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+
+	if (dir == DPLL_PIN_DIRECTION_INPUT) {
+		/* The internal INTSYNC and XO pins are always registrable */
+		if (index == SIT9531X_INTSYNC_PIN_ID ||
+		    index == SIT9531X_MAX_INPUTS)
+			return true;
+
+		return sit9531x_input_pin_is_registrable(sitdev, index);
+	}
+
+	/* The internal INTSYNC source pin is always registrable */
+	if (index == SIT9531X_INTSYNC_OUT_PIN_ID)
+		return true;
+
+	/* Output -- check if driven by this DPLL */
+	if (index >= sitdev->info->num_outputs)
+		return false;
+
+	/*
+	 * Routing, not the current drive state: an output the initial
+	 * configuration muted still gets a pin, so userspace can see it and
+	 * un-mute it.  Only outputs no PLL drives are left unregistered.
+	 */
+	return sitdev->out[index].pll_idx == sitdpll->id &&
+	       sitdev->out[index].routed;
+}
+
+/*
+ * sit9531x_dpll_pins_register - register all registrable pins
+ *
+ * Enumerates all possible input and output pins, checks registrability,
+ * and registers each one.  Input pins come first, then output pins,
+ * with input pins first, then output pins.
+ */
+static int sit9531x_dpll_pins_register(struct sit9531x_dpll *sitdpll)
+{
+	struct sit9531x_dpll_pin *pin;
+	enum dpll_pin_direction dir;
+	u8 id, index;
+	int rc;
+
+	for (index = 0; index < SIT9531X_NUM_PINS_TOTAL; index++) {
+		if (index < SIT9531X_NUM_INPUT_PINS) {
+			id = index;
+			dir = DPLL_PIN_DIRECTION_INPUT;
+		} else {
+			id = index - SIT9531X_NUM_INPUT_PINS;
+			dir = DPLL_PIN_DIRECTION_OUTPUT;
+		}
+
+		if (!sit9531x_dpll_pin_is_registrable(sitdpll, dir, id))
+			continue;
+
+		pin = sit9531x_dpll_pin_alloc(sitdpll, dir, id);
+		if (IS_ERR(pin)) {
+			rc = PTR_ERR(pin);
+			goto error;
+		}
+
+		rc = sit9531x_dpll_pin_register(pin, index);
+		if (rc) {
+			kfree(pin);
+			goto error;
+		}
+
+		list_add(&pin->list, &sitdpll->pins);
+	}
+
+	return 0;
+
+error:
+	sit9531x_dpll_pins_unregister(sitdpll);
+	return rc;
+}
+
+static int sit9531x_dpll_device_register(struct sit9531x_dpll *sitdpll)
+{
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	int rc;
+
+	sitdpll->ops = sit9531x_dpll_device_ops;
+
+	sitdpll->dpll_dev = dpll_device_get(sitdev->clock_id, sitdpll->id,
+					    THIS_MODULE, &sitdpll->tracker);
+	if (IS_ERR(sitdpll->dpll_dev)) {
+		rc = PTR_ERR(sitdpll->dpll_dev);
+		sitdpll->dpll_dev = NULL;
+		return rc;
+	}
+
+	rc = dpll_device_register(sitdpll->dpll_dev,
+				  sit9531x_prop_dpll_type_get(sitdev,
+							      sitdpll->id),
+				  &sitdpll->ops, sitdpll);
+	if (rc) {
+		dpll_device_put(sitdpll->dpll_dev, &sitdpll->tracker);
+		sitdpll->dpll_dev = NULL;
+	}
+
+	return rc;
+}
+
+static void sit9531x_dpll_device_unregister(struct sit9531x_dpll *sitdpll)
+{
+	dpll_device_unregister(sitdpll->dpll_dev, &sitdpll->ops, sitdpll);
+	dpll_device_put(sitdpll->dpll_dev, &sitdpll->tracker);
+	sitdpll->dpll_dev = NULL;
+}
+
+/*
+ * sit9531x_dpll_alloc - allocate a DPLL device structure
+ * @sitdev:	parent device
+ * @ch:		PLL channel number (0-3)
+ *
+ * Return: pointer to allocated DPLL on success, error pointer on error
+ */
+struct sit9531x_dpll *sit9531x_dpll_alloc(struct sit9531x_dev *sitdev, u8 ch)
+{
+	struct sit9531x_dpll *sitdpll;
+
+	sitdpll = kzalloc_obj(*sitdpll, GFP_KERNEL);
+	if (!sitdpll)
+		return ERR_PTR(-ENOMEM);
+
+	sitdpll->dev = sitdev;
+	sitdpll->id = ch;
+	sitdpll->lock_status = DPLL_LOCK_STATUS_UNLOCKED;
+	INIT_LIST_HEAD(&sitdpll->pins);
+
+	return sitdpll;
+}
+
+/*
+ * sit9531x_dpll_free - deallocate a DPLL device structure
+ * @sitdpll:	DPLL to free
+ */
+void sit9531x_dpll_free(struct sit9531x_dpll *sitdpll)
+{
+	kfree(sitdpll);
+}
+
+/*
+ * sit9531x_dpll_register - register DPLL device and all its pins
+ *
+ * Registers the DPLL device with the subsystem and then registers
+ * all input and output pins that are connected to this PLL.
+ */
+int sit9531x_dpll_register(struct sit9531x_dpll *sitdpll)
+{
+	int rc;
+
+	rc = sit9531x_dpll_device_register(sitdpll);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_dpll_pins_register(sitdpll);
+	if (rc) {
+		sit9531x_dpll_device_unregister(sitdpll);
+		return rc;
+	}
+
+	return 0;
+}
+
+/* sit9531x_dpll_unregister - unregister DPLL device and its pins */
+void sit9531x_dpll_unregister(struct sit9531x_dpll *sitdpll)
+{
+	sit9531x_dpll_pins_unregister(sitdpll);
+	sit9531x_dpll_device_unregister(sitdpll);
+}
+
+static void sit9531x_dpll_list_free(struct sit9531x_dev *sitdev)
+{
+	struct sit9531x_dpll *sitdpll, *next;
+
+	list_for_each_entry_safe(sitdpll, next, &sitdev->dplls, list) {
+		list_del(&sitdpll->list);
+		sit9531x_dpll_free(sitdpll);
+	}
+}
+
+/* Runs only once the device is fully started, see the caller. */
+static void sit9531x_dev_dpll_fini(void *ptr)
+{
+	struct sit9531x_dev *sitdev = ptr;
+
+	sit9531x_dev_stop(sitdev);
+	kthread_destroy_worker(sitdev->kworker);
+	sit9531x_dpll_list_free(sitdev);
+}
+
+static int sit9531x_devm_dpll_init(struct sit9531x_dev *sitdev)
+{
+	struct kthread_worker *kworker;
+	struct sit9531x_dpll *sitdpll;
+	unsigned int i;
+	int rc;
+
+	INIT_LIST_HEAD(&sitdev->dplls);
+	kthread_init_delayed_work(&sitdev->work, sit9531x_dev_periodic_work);
+
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		sitdpll = sit9531x_dpll_alloc(sitdev, i);
+		if (IS_ERR(sitdpll)) {
+			rc = dev_err_probe(sitdev->dev, PTR_ERR(sitdpll),
+					   "Failed to alloc DPLL%u\n", i);
+			goto err_free_dplls;
+		}
+
+		list_add_tail(&sitdpll->list, &sitdev->dplls);
+	}
+
+	kworker = kthread_run_worker(0, "sit9531x-%s", dev_name(sitdev->dev));
+	if (IS_ERR(kworker)) {
+		rc = PTR_ERR(kworker);
+		goto err_free_dplls;
+	}
+	sitdev->kworker = kworker;
+
+	rc = sit9531x_dev_start(sitdev);
+	if (rc) {
+		rc = dev_err_probe(sitdev->dev, rc, "Failed to start device\n");
+		goto err_destroy_worker;
+	}
+
+	/*
+	 * Only now is every field the cleanup touches valid, so this is the
+	 * first point at which the action may be registered.  On failure it
+	 * runs the action itself, which is correct here and only here.
+	 */
+	return devm_add_action_or_reset(sitdev->dev, sit9531x_dev_dpll_fini,
+					sitdev);
+
+err_destroy_worker:
+	kthread_destroy_worker(sitdev->kworker);
+err_free_dplls:
+	sit9531x_dpll_list_free(sitdev);
+
+	return rc;
+}
+
+/*
+ * sit9531x_read_variant_id - read chip variant ID byte from hardware
+ * @id:		output variant ID byte
+ *
+ * Reads the single-byte variant identification register from Page 0
+ * reg 0x02 (95317 = 0x17, 95316 = 0x31).  Reg 0x03 holds a separate
+ * revision byte and is intentionally not consumed here.
+ */
+static int sit9531x_read_variant_id(struct sit9531x_dev *sitdev, u8 *id)
+{
+	return sit9531x_read_u8(sitdev, SIT9531X_REG_VARIANT_ID, id);
+}
+
+static const struct sit9531x_chip_info *sit9531x_match_variant(u8 id)
+{
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(sit9531x_chip_ids); i++) {
+		if (sit9531x_chip_ids[i].id == id)
+			return &sit9531x_chip_ids[i];
+	}
+
+	return NULL;
+}
+
+/*
+ * sit9531x_derive_clock_id - build EUI-64 clock identifier
+ *
+ * Generates a deterministic 64-bit identifier from the SiTime OUI,
+ * the chip ID, and the I2C address.  This provides a stable clock_id
+ * across reboots.
+ *
+ * Return: 64-bit clock identifier
+ */
+static u64 sit9531x_derive_clock_id(struct sit9531x_dev *sitdev)
+{
+	u64 clkid;
+
+	clkid  = SIT9531X_OUI << 24;
+	clkid |= (u64)sitdev->info->id << 8;
+	clkid |= (u64)sitdev->client->addr;
+
+	return clkid;
+}
+
+int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
+{
+	struct clk *xtal_clk;
+	u8 variant_id;
+	int rc;
+
+	/*
+	 * Fvco = Fref * (DIVN + frac/2^32) with Fref = xtal_freq << doubler,
+	 * so every freq_set and phase_adjust path divides by a rate derived
+	 * from the XO feeding XIN/XO_CLK.
+	 */
+	xtal_clk = devm_clk_get_enabled(sitdev->dev, "xtal");
+	if (IS_ERR(xtal_clk))
+		return dev_err_probe(sitdev->dev, PTR_ERR(xtal_clk),
+				     "Failed to get xtal clock\n");
+	sitdev->xtal_freq = clk_get_rate(xtal_clk);
+	if (!sitdev->xtal_freq)
+		return dev_err_probe(sitdev->dev, -EINVAL,
+				     "xtal clock has no rate\n");
+
+	/*
+	 * Held deasserted, never pulsed: the chip configuration comes from
+	 * efuse or an NVM blob applied before probe, and a reset would
+	 * discard it.  Must precede the first I2C access, as a board that
+	 * powers up asserted keeps the chip unreachable until released.
+	 */
+	sitdev->reset_gpio = devm_gpiod_get_optional(sitdev->dev, "reset",
+						     GPIOD_OUT_LOW);
+	if (IS_ERR(sitdev->reset_gpio))
+		return dev_err_probe(sitdev->dev, PTR_ERR(sitdev->reset_gpio),
+				     "Failed to request reset gpio\n");
+	if (sitdev->reset_gpio)
+		fsleep(10000);	/* internal boot after release */
+
+	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;
 }
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 230b21b9e238..d4159c7ddaf5 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,16 @@
  */
 #define SIT9531X_INTSYNC_PIN_ID		(SIT9531X_MAX_INPUTS + 1)
 #define SIT9531X_INTSYNC_OUT_PIN_ID	SIT9531X_MAX_OUTPUTS
+#define SIT9531X_NUM_PINS		(SIT9531X_MAX_INPUTS + 2 + SIT9531X_MAX_OUTPUTS + 1)
+#define SIT9531X_STATUS_POLL_MS		500
+
+/* selected_ref value when the active source is not a registered input */
+#define SIT9531X_REF_INVALID		0xFF
+
+/* SiTime IEEE OUI for EUI-64 generation */
+#define SIT9531X_OUI			0x0090C2FFFEULL
+
+struct sit9531x_dpll;
 
 /*
  * struct sit9531x_chip_info - chip variant identification
@@ -64,37 +78,94 @@ 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;
+	u32		freq;
+	bool		enabled;
+	bool		los;
+	bool		oof;
+	u8		pll_mask;
+	const char	*label;
 	enum sit9531x_signal_mode	sig_mode;
 };
 
 /*
  * struct sit9531x_out - output state
- * @freq:		configured frequency in Hz
+ * @enabled:		output is driving, i.e. not forced into Hi-Z
+ * @routed:		output is mapped to @pll_idx by the initial
+ *			configuration; an unrouted output has no DPLL pin
+ * @pll_idx:		PLL driving this output (0-3)
  * @label:		board label from DT or default
  */
 struct sit9531x_out {
 	u32		freq;
+	bool		enabled;
+	bool		routed;
+	u8		pll_idx;
 	const char	*label;
 };
 
+/*
+ * struct sit9531x_chan - per-PLL channel state
+ * @active:		PLL has reached its active state; a PLL the loaded
+ *			configuration leaves unused never does, and its
+ *			loss-of-lock bit stays clear because nothing drives it
+ * @locked:		PLL is locked (raw status register bit)
+ * @mode:		0 = sync (outer loop enabled), 1 = free-run
+ * @selected_ref:	logical input index of the currently selected
+ *			reference (the INTSYNC net maps to
+ *			SIT9531X_INTSYNC_PIN_ID), or SIT9531X_REF_INVALID
+ *			when the hardware source encoding is reserved
+ * @inner_lol:		PLL inner loop loss-of-lock detected
+ * @ho_freeze:		holdover freeze active
+ * @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 +178,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 +304,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..821370ddeab0
--- /dev/null
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -0,0 +1,417 @@
+// 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 inline bool sit9531x_dpll_is_input_pin(const struct sit9531x_dpll_pin *pin)
+{
+	return pin->dir == DPLL_PIN_DIRECTION_INPUT;
+}
+
+static inline bool
+sit9531x_dpll_is_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;
+
+	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);
+
+	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) {
+		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,
+	/*
+	 * 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),
+};
+
+/*
+ * 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..e5eef4514bcd
--- /dev/null
+++ b/drivers/dpll/sit9531x/dpll.h
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * SiTime SiT9531x DPLL subsystem interface
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * DPLL device and pin structures, and function declarations for
+ * the DPLL registration and callback layer.
+ */
+
+#ifndef _SIT9531X_DPLL_H
+#define _SIT9531X_DPLL_H
+
+#include <linux/dpll.h>
+#include <linux/list.h>
+#include <linux/types.h>
+
+struct sit9531x_dev;
+
+/* Per-pin DPLL state. */
+struct sit9531x_dpll_pin {
+	struct list_head		list;
+	struct sit9531x_dpll		*dpll;
+	struct dpll_pin			*dpll_pin;
+	dpll_tracker			tracker;
+	struct fwnode_handle		*fwnode;
+	char				label[8];	/* "IN0", "OUT3" */
+	enum dpll_pin_direction		dir;
+	u8				id;		/* hardware index */
+	u8				prio;
+	enum dpll_pin_state		pin_state;
+	s32				phase_adjust;	/* picoseconds */
+	s64				phase_offset;	/* picoseconds */
+	bool				esync_control;
+	u64				esync_freq;	/* 0 == disabled */
+};
+
+/* Per-PLL DPLL device state. */
+struct sit9531x_dpll {
+	struct list_head		list;
+	struct sit9531x_dev		*dev;
+	struct dpll_device		*dpll_dev;
+	dpll_tracker			tracker;
+	struct dpll_device_ops		ops;	/* per-instance copy */
+	struct list_head		pins;
+	u8				id;	/* 0 = PLLA .. 3 = PLLD */
+	enum dpll_lock_status		lock_status;
+};
+
+/* ---- DPLL allocation and registration ---- */
+/*
+ * The callback tables stay with the callbacks; the registration code that
+ * hands them to the subsystem lives next to probe() in core.c.
+ */
+extern const struct dpll_device_ops sit9531x_dpll_device_ops;
+const struct dpll_pin_ops *
+sit9531x_dpll_pin_ops_get(const struct sit9531x_dpll_pin *pin);
+
+struct sit9531x_dpll *sit9531x_dpll_alloc(struct sit9531x_dev *sitdev, u8 ch);
+void sit9531x_dpll_free(struct sit9531x_dpll *sitdpll);
+int  sit9531x_dpll_register(struct sit9531x_dpll *sitdpll);
+void sit9531x_dpll_unregister(struct sit9531x_dpll *sitdpll);
+
+/* ---- Periodic change detection ---- */
+void sit9531x_dpll_changes_check(struct sit9531x_dpll *sitdpll);
+
+#endif /* _SIT9531X_DPLL_H */
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index 8337f1235472..705176c35ddd 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -42,10 +42,6 @@
 #define SIT9531X_PAGE_PLLD			0x0D
 #define SIT9531X_PAGE_PLLD_EXT		0x1D
 
-/* PLL index to page mapping */
-#define SIT9531X_PLL_PAGE(_idx) \
-	(SIT9531X_PAGE_PLLA + (_idx))
-
 /*
  * VARIANT_ID is a single byte at page 0 reg 0x02 (95317 = 0x17, 95316 = 0x31).
  * Reg 0x03 carries an unrelated revision byte and must not be combined into
@@ -53,7 +49,224 @@
  */
 #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, 4=IN4P,
+ *   5=OCXO, 6=INTSYNC,
+ *   7=IN0N, 8=IN1N, 9=IN2N, 10=IN3N, 11=IN4N
+ */
+#define SIT9531X_PAGE_PRIOSYS		0x01
+#define SIT9531X_PRIO_BASE_REG		0x16
+#define SIT9531X_PRIO_REGS_PER_PLL		6
+#define SIT9531X_PRIO_SLOTS_PER_REG		2
+/*
+ * 11 priority slots, CLK_SPARE0SEL_PLL through CLK_SPARE10SEL_PLL.
+ * The twelfth nibble of the block is not a slot: it is
+ * CLK_ACTIVESEL_PLL, see SIT9531X_PRIO_ACTIVESEL_OFF below.
+ */
+#define SIT9531X_PRIO_MAX_SLOTS		11
+/* Number of source encodings (0-11), unrelated to the slot count */
+#define SIT9531X_PRIO_NUM_SRC		12
+#define SIT9531X_PRIO_NIBBLE_MASK		0x0F
+#define SIT9531X_PRIO_HI_SHIFT		4
+/* Input source encoding values (see table above) */
+#define SIT9531X_PRIO_SRC_OCXO		5
+#define SIT9531X_PRIO_SRC_INTSYNC		6
+#define SIT9531X_PRIO_SRC_N_BASE		7
+/*
+ * The last register of each PLL's priority block holds, in its low
+ * nibble, the input source the PLL has currently selected as its
+ * active reference (CLK_ACTIVESEL_PLL, same 4-bit encoding as above).
+ */
+#define SIT9531X_PRIO_ACTIVESEL_OFF		5
+
+/*
+ * Page 0 -- PRG_Directives_GENERIC_0, the main system's programming
+ * directive register.  Every page carries its own copy of this
+ * register at offset 0x0F with the same bit layout:
+ *
+ *   bit 6  proceed to loop lock / active state from the PRG_CMD state
+ *   bit 4  update the NVM bank from the efuse contents
+ *   bit 3  read the efuse 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 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.43.0


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

* [PATCH net-next v8 07/15] dpll: sit9531x: add support to get and set priority on input pins
  2026-09-02 21:40 [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (4 preceding siblings ...)
  2026-09-02 21:40 ` [PATCH net-next v8 04/15] dpll: sit9531x: read DPLL types and pin properties from system firmware Ali Rouhi
@ 2026-09-02 21:40 ` Ali Rouhi
  2026-09-08 12:45   ` netdev-bot+sashiko
  2026-09-02 21:40 ` [PATCH net-next v8 06/15] dpll: sit9531x: implement input pin state on a DPLL Ali Rouhi
                   ` (9 subsequent siblings)
  15 siblings, 1 reply; 31+ messages in thread
From: Ali Rouhi @ 2026-09-02 21:40 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, 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 | 43 +++++++++++++++++++
 drivers/dpll/sit9531x/dpll.c | 80 ++++++++++++++++++++++++++++++++++++
 2 files changed, 123 insertions(+)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index f42c41d8a42e..e706f6942f65 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -414,6 +414,49 @@ static int sit9531x_prio_prg_commit(struct sit9531x_dev *sitdev)
 	return 0;
 }
 
+/*
+ * sit9531x_input_prio_get - read an input's priority slot for a PLL
+ * @input_idx:	input source in hardware encoding (see
+ *		sit9531x_input_hw_src())
+ * @prio:	output slot position (0 = highest); set to
+ *		SIT9531X_PRIO_MAX_SLOTS when the source is not in the table
+ *
+ * Scans the PLL's 12-slot priority table on Page 1 and returns the
+ * highest-priority (lowest-numbered) slot that references the source.
+ * This reads the value the chip actually holds rather than a cached
+ * default, so pin-get reflects the real hardware priority.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_input_prio_get(struct sit9531x_dev *sitdev, u8 pll_idx,
+			    u8 input_idx, u8 *prio)
+{
+	u8 val, slot, src;
+	int rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+		rc = sit9531x_read_u8(sitdev,
+				      sit9531x_prio_reg(pll_idx, slot), &val);
+		if (rc)
+			return rc;
+
+		src = sit9531x_prio_slot_get(val, slot);
+
+		if (src == input_idx) {
+			*prio = slot;
+			return 0;
+		}
+	}
+
+	*prio = SIT9531X_PRIO_MAX_SLOTS;
+	return 0;
+}
+
 /*
  * 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
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index 29088707a3e6..67b9fbba9f2e 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -417,10 +417,90 @@ 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
+ *
+ * reads the PLL's priority table on Page 1 (via
+ * sit9531x_input_prio_get()) and returns the slot the input
+ * occupies, so pin-get reports the real hardware priority rather
+ * than a software default.
+ */
+static int
+sit9531x_dpll_input_pin_prio_get(const struct dpll_pin *pin, void *pin_priv,
+				 const struct dpll_device *dpll, void *dpll_priv,
+				 u32 *prio, struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	u8 slot;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_input_prio_get(sitdev, sitdpll->id,
+				     sit9531x_input_hw_src(dpin->id), &slot);
+	mutex_unlock(&sitdev->multiop_lock);
+	if (rc)
+		return rc;
+
+	dpin->prio = slot;
+	*prio = slot;
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_input_pin_prio_set - set input pin priority
+ *
+ * writes input priority table on Page 1 via
+ * core.c sit9531x_input_prio_set().  Forces holdover during update.
+ */
+static int
+sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
+				 const struct dpll_device *dpll, void *dpll_priv,
+				 u32 prio, struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	int rc;
+
+	if (dpin->dir != DPLL_PIN_DIRECTION_INPUT) {
+		NL_SET_ERR_MSG(extack, "Priority applies only to input pins");
+		return -EINVAL;
+	}
+
+	if (prio >= SIT9531X_PRIO_MAX_SLOTS) {
+		NL_SET_ERR_MSG(extack, "Priority out of range (0-10)");
+		return -EINVAL;
+	}
+
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_input_prio_set(sitdev, sitdpll->id,
+				     sit9531x_input_hw_src(dpin->id),
+				     (u8)prio);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc == -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;
+	}
+
+	dpin->prio = (u8)prio;
+
+	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,
 	/*
 	 * The measurement compares the PLL's running feedback divider with
 	 * its configured one, so it describes the device's own reference
-- 
2.43.0


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

* [PATCH net-next v8 06/15] dpll: sit9531x: implement input pin state on a DPLL
  2026-09-02 21:40 [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (5 preceding siblings ...)
  2026-09-02 21:40 ` [PATCH net-next v8 07/15] dpll: sit9531x: add support to get and set priority on input pins Ali Rouhi
@ 2026-09-02 21:40 ` Ali Rouhi
  2026-09-08 12:45   ` netdev-bot+sashiko
  2026-09-02 21:40 ` [PATCH net-next v8 09/15] dpll: sit9531x: implement output " Ali Rouhi
                   ` (8 subsequent siblings)
  15 siblings, 1 reply; 31+ messages in thread
From: Ali Rouhi @ 2026-09-02 21:40 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, 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.

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 | 330 +++++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/dpll.c | 152 ++++++++++++++++
 drivers/dpll/sit9531x/regs.h |   4 +
 3 files changed, 486 insertions(+)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 1394ea1077d9..f42c41d8a42e 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -184,6 +184,73 @@ 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.
+ */
+int sit9531x_input_disable(struct sit9531x_dev *sitdev, u8 index)
+{
+	struct sit9531x_ref *ref = &sitdev->ref[index];
+	unsigned int force_reg, state_reg;
+	u8 pair = sit9531x_input_pair(index);
+	u8 val;
+	int rc;
+
+	sit9531x_input_get_regs(sitdev, index, &force_reg, &state_reg);
+
+	rc = sit9531x_read_u8(sitdev, force_reg, &val);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, force_reg, val | BIT(pair));
+	if (rc)
+		return rc;
+
+	rc = sit9531x_read_u8(sitdev, state_reg, &val);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, state_reg, val & ~BIT(pair));
+	if (rc)
+		return rc;
+
+	ref->enabled = false;
+
+	return 0;
+}
+
+/*
+ * sit9531x_input_enable - enable an input reference
+ * @index:	logical input index (0-N)
+ *
+ * Clears the force mask bit for the given input, returning it to
+ * hardware default (enabled).
+ */
+int sit9531x_input_enable(struct sit9531x_dev *sitdev, u8 index)
+{
+	struct sit9531x_ref *ref = &sitdev->ref[index];
+	unsigned int force_reg, state_reg;
+	u8 pair = sit9531x_input_pair(index);
+	u8 val;
+	int rc;
+
+	sit9531x_input_get_regs(sitdev, index, &force_reg, &state_reg);
+
+	rc = sit9531x_read_u8(sitdev, force_reg, &val);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, force_reg, val & ~BIT(pair));
+	if (rc)
+		return rc;
+
+	ref->enabled = true;
+
+	return 0;
+}
+
 /*
  * Output enable / disable (Hi-Z control)
  *
@@ -310,6 +377,43 @@ 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 vendor input_priority_sel() procedure.
+ */
+static int sit9531x_prio_prg_commit(struct sit9531x_dev *sitdev)
+{
+	int rc;
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+			       SIT9531X_SMALL_UPDATE_CMD);
+	if (rc)
+		return rc;
+
+	usleep_range(1000, 2000);
+
+	return 0;
+}
+
 /*
  * 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
@@ -328,6 +432,67 @@ static void sit9531x_prio_mask_build(struct sit9531x_dev *sitdev, u8 pll_idx,
 	sitdev->chan[pll_idx].prio_mask = mask;
 }
 
+/*
+ * sit9531x_prio_table_commit - write a full priority table for a PLL
+ * @srcs:	array of SIT9531X_PRIO_MAX_SLOTS source codes, slot 0 first
+ *
+ * Programs all priority slots (nibble-packed, two per register) for
+ * the PLL using the same holdover / small-update sequence as
+ * sit9531x_input_prio_set().  Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_prio_table_commit(struct sit9531x_dev *sitdev, u8 pll_idx,
+				      const u8 *srcs)
+{
+	u8 val, slot;
+	int rc, prg_rc, ho_rc;
+	u16 reg;
+
+	rc = sit9531x_update_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_HO_CTRL,
+				    BIT(SIT9531X_PLL_HO_FORCE_BIT),
+				    BIT(SIT9531X_PLL_HO_FORCE_BIT));
+	if (rc)
+		return rc;
+
+	usleep_range(10000, 12000);
+
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+		reg = sit9531x_prio_reg(pll_idx, slot);
+
+		rc = sit9531x_read_u8(sitdev, reg, &val);
+		if (rc)
+			goto commit;
+
+		val = sit9531x_prio_slot_set(val, slot, srcs[slot]);
+
+		rc = sit9531x_write_u8(sitdev, reg, val);
+		if (rc)
+			goto commit;
+	}
+
+commit:
+	/* Latch unconditionally, as in sit9531x_input_prio_set(). */
+	prg_rc = sit9531x_prio_prg_commit(sitdev);
+	if (prg_rc && !rc)
+		rc = prg_rc;
+
+	/*
+	 * 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);
+
+	ho_rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+				       SIT9531X_PLL_REG_HO_CTRL,
+				       BIT(SIT9531X_PLL_HO_FORCE_BIT), 0);
+	if (ho_rc && !rc)
+		rc = ho_rc;
+
+	return rc;
+}
+
 /*
  * sit9531x_prio_table_read - read a PLL's priority-table source codes
  * @srcs:	output array of SIT9531X_PRIO_MAX_SLOTS source codes
@@ -352,6 +517,171 @@ 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 srcs[SIT9531X_PRIO_MAX_SLOTS];
+	u8 slot, from;
+	int rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+	if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+		return -EINVAL;
+	if (prio >= SIT9531X_PRIO_MAX_SLOTS)
+		return -EINVAL;
+
+	rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+	if (rc)
+		return rc;
+
+	for (from = 0; from < SIT9531X_PRIO_MAX_SLOTS; from++)
+		if (srcs[from] == input_idx)
+			break;
+
+	if (from == 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;
+
+	return sit9531x_prio_table_commit(sitdev, pll_idx, srcs);
+}
+
+/*
+ * sit9531x_input_prio_remove - drop an input from a PLL's priority table
+ * @input_idx:	input source in hardware encoding
+ *
+ * Rewrites the priority table with the source removed: the remaining
+ * sources are compacted toward the highest-priority slots and the freed
+ * tail slots are backfilled with the lowest-priority remaining source
+ * (matching sit9531x_input_prio_set()).  This makes a disconnected
+ * input ineligible for automatic reference selection, not just gated at
+ * the input buffer.
+ *
+ * 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;
+
+	rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+	if (rc)
+		return rc;
+
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+		if (srcs[slot] == input_idx)
+			found = true;
+		else
+			kept[count++] = srcs[slot];
+	}
+
+	if (!found)
+		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 is
+ * placed in the lowest-priority slot.  The original priority is not
+ * restored -- use sit9531x_input_prio_set() to reassign it.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
+			    u8 input_idx)
+{
+	u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+	u8 slot;
+	int rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+	if (rc)
+		return rc;
+
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++)
+		if (srcs[slot] == input_idx)
+			return 0;
+
+	srcs[SIT9531X_PRIO_MAX_SLOTS - 1] = input_idx;
+
+	return sit9531x_prio_table_commit(sitdev, pll_idx, srcs);
+}
+
 /* XO doubler register */
 #define SIT9531X_REG_XO2_GENERIC		SIT9531X_REG(0x00, 0x2D)
 #define SIT9531X_XO_DOUBLER_ENB_BIT		7   /* inverted: 0 = enabled */
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index 821370ddeab0..29088707a3e6 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -255,6 +255,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,
@@ -267,8 +302,125 @@ 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);
+		if (rc)
+			break;
+		ref->pll_mask &= ~pll_bit;
+		if (ref->pll_mask)
+			rc = 0;	/* another DPLL still uses this input */
+		else
+			rc = sit9531x_input_disable(sitdev, dpin->id);
+		break;
+	case DPLL_PIN_STATE_CONNECTED:
+		/*
+		 * CONNECTED asks for this input and no other, 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:
+		rc = sit9531x_input_enable(sitdev, dpin->id);
+		if (rc)
+			break;
+		rc = sit9531x_input_prio_add(sitdev, sitdpll->id, hw_src);
+		if (rc)
+			break;
+		/*
+		 * Claim the input for this DPLL only once it is both enabled
+		 * and present in the priority table.  Setting the mask before
+		 * prio_add would leak the claim if prio_add failed, keeping the
+		 * shared input receiver powered even after every DPLL released
+		 * it.
+		 */
+		ref->pll_mask |= pll_bit;
+		break;
+	default:
+		rc = -EINVAL;
+		break;
+	}
+
+	mutex_unlock(&sitdev->multiop_lock);
+
+	/*
+	 * 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 && 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,
 	/*
 	 * The measurement compares the PLL's running feedback divider with
 	 * its configured one, so it describes the device's own reference
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index 705176c35ddd..dd027142b370 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -132,6 +132,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.43.0


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

* [PATCH net-next v8 09/15] dpll: sit9531x: implement output pin state on a DPLL
  2026-09-02 21:40 [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (6 preceding siblings ...)
  2026-09-02 21:40 ` [PATCH net-next v8 06/15] dpll: sit9531x: implement input pin state on a DPLL Ali Rouhi
@ 2026-09-02 21:40 ` Ali Rouhi
  2026-09-08 12:45   ` netdev-bot+sashiko
  2026-09-02 21:40 ` [PATCH net-next v8 08/15] dpll: sit9531x: add support to get and set frequency on pins Ali Rouhi
                   ` (7 subsequent siblings)
  15 siblings, 1 reply; 31+ messages in thread
From: Ali Rouhi @ 2026-09-02 21:40 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, 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 | 143 +++++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/dpll.c |  69 +++++++++++++++++
 2 files changed, 212 insertions(+)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 721b2c451d59..0f886cfd1401 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -339,6 +339,40 @@ static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev, u8 slot,
 	return 0;
 }
 
+static int sit9531x_hiz_set_bit(struct sit9531x_dev *sitdev,
+				unsigned int reg, u8 bit, bool set)
+{
+	u8 cur, new_val;
+	int rc;
+
+	rc = sit9531x_read_u8(sitdev, reg, &cur);
+	if (rc)
+		return rc;
+
+	new_val = set ? (cur | BIT(bit)) : (cur & ~BIT(bit));
+
+	return sit9531x_write_u8(sitdev, reg, new_val);
+}
+
+/*
+ * Enter the output-system programming state: unlock the debug
+ * registers on Page 3 and issue the PRG_CMD state command.  Register
+ * writes that reconfigure the output system only take effect when
+ * they are made inside this state.
+ */
+static int sit9531x_prg_enter(struct sit9531x_dev *sitdev)
+{
+	int rc;
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
+			       SIT9531X_DEBUG_UNLOCK_VAL);
+	if (rc)
+		return rc;
+
+	return sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+				 SIT9531X_PRG_CMD_STATE);
+}
+
 /*
  * Commit a programming sequence started by sit9531x_prg_enter():
  * update the NVM shadow and re-lock the loops.  The sleep gives the
@@ -368,6 +402,115 @@ static int sit9531x_prg_commit(struct sit9531x_dev *sitdev)
 	return rc ? rc : rc2;
 }
 
+/*
+ * sit9531x_output_disable - mute an output (force Hi-Z)
+ * @index:	logical output index (0..info->num_outputs-1)
+ *
+ * Sets MASK+STATE on BOTH the DIFF and SE register pairs so that the
+ * output is muted regardless of its electrical configuration.  The
+ * writes are wrapped in the PRG_CMD / NVM update / loop lock sequence
+ * so the new state is applied by the hardware.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_output_disable(struct sit9531x_dev *sitdev, u8 index)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	struct sit9531x_hiz_regs r;
+	u8 slot;
+	int rc, ret;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (index >= info->num_outputs)
+		return -EINVAL;
+
+	slot = info->clkout_map[index];
+	sit9531x_output_get_hiz_regs(slot, &r);
+
+	rc = sit9531x_prg_enter(sitdev);
+	if (rc)
+		return rc;
+
+	/* Take control (MASK=1) and mute (STATE=0) on both DIFF and SE */
+	rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, true);
+	if (rc)
+		goto commit;
+	rc = sit9531x_hiz_set_bit(sitdev, r.diff_state, r.bit, false);
+	if (rc)
+		goto commit;
+	rc = sit9531x_hiz_set_bit(sitdev, r.se_mask, r.bit, true);
+	if (rc)
+		goto commit;
+	rc = sit9531x_hiz_set_bit(sitdev, r.se_state, r.bit, false);
+
+commit:
+	/*
+	 * Always leave the PRG_CMD programming state, even on a mid-sequence
+	 * write failure: prg_enter() unlocked the output loops, so returning
+	 * without prg_commit() would strand the chip in the programming state
+	 * with the loops unlocked.  Best effort -- keep the first error.
+	 */
+	ret = sit9531x_prg_commit(sitdev);
+	if (ret && !rc)
+		rc = ret;
+	if (!rc)
+		sitdev->out[index].enabled = false;
+
+	return rc;
+}
+
+/*
+ * sit9531x_output_enable - un-mute an output (active state)
+ * @index:	logical output index (0..info->num_outputs-1)
+ *
+ * Releases MASK on BOTH register pairs so the output returns to
+ * whatever the initial_config blob programmed.  The writes are wrapped
+ * in the PRG_CMD / NVM update / loop lock sequence so the new state is
+ * applied by the hardware.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_output_enable(struct sit9531x_dev *sitdev, u8 index)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	struct sit9531x_hiz_regs r;
+	u8 slot;
+	int rc, ret;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (index >= info->num_outputs)
+		return -EINVAL;
+
+	slot = info->clkout_map[index];
+	sit9531x_output_get_hiz_regs(slot, &r);
+
+	rc = sit9531x_prg_enter(sitdev);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, false);
+	if (rc)
+		goto commit;
+	rc = sit9531x_hiz_set_bit(sitdev, r.se_mask, r.bit, false);
+
+commit:
+	/*
+	 * Always leave the PRG_CMD programming state, even on a mid-sequence
+	 * write failure: prg_enter() unlocked the output loops, so returning
+	 * without prg_commit() would strand the chip in the programming state
+	 * with the loops unlocked.  Best effort -- keep the first error.
+	 */
+	ret = sit9531x_prg_commit(sitdev);
+	if (ret && !rc)
+		rc = ret;
+	if (!rc)
+		sitdev->out[index].enabled = true;
+
+	return rc;
+}
+
 /*
  * Input priority selection
  *
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index a7510ca721b6..6365a83e4c52 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -669,10 +669,79 @@ 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;
+	const struct sit9531x_out *out;
+
+	out = sit9531x_out_state_get(sitdpll->dev, dpin->id);
+	*state = out->enabled ? DPLL_PIN_STATE_CONNECTED
+			      : DPLL_PIN_STATE_DISCONNECTED;
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_output_pin_state_on_dpll_set - mute/un-mute an output
+ *
+ * forces Hi-Z on the output pin via the Page 0x03
+ * force/state register pair.
+ *   CONNECTED    -> enable (release force, back to factory default)
+ *   DISCONNECTED -> disable (force Hi-Z)
+ */
+static int
+sit9531x_dpll_output_pin_state_on_dpll_set(const struct dpll_pin *pin,
+					   void *pin_priv,
+					   const struct dpll_device *dpll,
+					   void *dpll_priv,
+					   enum dpll_pin_state state,
+					   struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	switch (state) {
+	case DPLL_PIN_STATE_CONNECTED:
+		rc = sit9531x_output_enable(sitdev, dpin->id);
+		break;
+	case DPLL_PIN_STATE_DISCONNECTED:
+		rc = sit9531x_output_disable(sitdev, dpin->id);
+		break;
+	default:
+		rc = -EINVAL;
+		break;
+	}
+
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc)
+		NL_SET_ERR_MSG(extack, "Failed to set output pin state");
+
+	return rc;
+}
+
 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.43.0


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

* [PATCH net-next v8 08/15] dpll: sit9531x: add support to get and set frequency on pins
  2026-09-02 21:40 [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (7 preceding siblings ...)
  2026-09-02 21:40 ` [PATCH net-next v8 09/15] dpll: sit9531x: implement output " Ali Rouhi
@ 2026-09-02 21:40 ` Ali Rouhi
  2026-09-08 12:45   ` netdev-bot+sashiko
  2026-09-02 21:40 ` [PATCH net-next v8 10/15] dpll: sit9531x: add support to adjust output phase Ali Rouhi
                   ` (6 subsequent siblings)
  15 siblings, 1 reply; 31+ messages in thread
From: Ali Rouhi @ 2026-09-02 21:40 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, 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.

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 | 442 +++++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/dpll.c |  95 ++++++++
 drivers/dpll/sit9531x/regs.h |  16 ++
 3 files changed, 553 insertions(+)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index e706f6942f65..721b2c451d59 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -339,6 +339,35 @@ static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev, u8 slot,
 	return 0;
 }
 
+/*
+ * 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;
+
+	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.
+	 */
+	rc2 = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+				SIT9531X_LOOP_LOCK);
+
+	msleep(100);
+
+	return rc ? rc : rc2;
+}
+
 /*
  * Input priority selection
  *
@@ -725,6 +754,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 */
@@ -735,6 +769,414 @@ int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
 #define SIT9531X_FVCO_HIGHBAND_MIN		6875000000ULL
 #define SIT9531X_FVCO_HIGHBAND_MAX		7812500000ULL
 
+/*
+ * sit9531x_is_xo_doubler_enabled - check if Fref doubler is active
+ *
+ * Register 0x2D bit 7 is active-low: 0 = doubler enabled, 1 = disabled.
+ *
+ * Return: 1 if enabled, 0 if disabled, <0 on error
+ */
+static int sit9531x_is_xo_doubler_enabled(struct sit9531x_dev *sitdev)
+{
+	u8 val;
+	int rc;
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_XO2_GENERIC, &val);
+	if (rc)
+		return rc;
+
+	return (~val >> SIT9531X_XO_DOUBLER_ENB_BIT) & 1u;
+}
+
+/*
+ * 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; the previous split
+ * between free-run and sync formulas returned 0 on chips that didn't
+ * have a sync input populated, which broke the TDC phase readback.
+ *
+ * Return: Fvco in Hz, or 0 on error
+ */
+static u64 sit9531x_get_fvco(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+	int doubler, rc;
+	s64 divn;
+	u64 fref;
+
+	/*
+	 * DT board-config override: some configs (e.g. an INTSYNC PLL)
+	 * run a VCO that Fref*DIVN does not reproduce.  When the board
+	 * supplies the measured VCO, use it verbatim.
+	 */
+	if (pll_idx < SIT9531X_NUM_PLLS && sitdev->pll_fvco[pll_idx])
+		return sitdev->pll_fvco[pll_idx];
+
+	rc = sit9531x_divn_static(sitdev, pll_idx, &divn);
+	if (rc || divn <= 0)
+		return 0;
+
+	doubler = sit9531x_is_xo_doubler_enabled(sitdev);
+	if (doubler < 0)
+		return 0;
+
+	fref = (u64)sitdev->xtal_freq << doubler;
+
+	return mul_u64_u64_div_u64(fref, (u64)divn, SIT9531X_DIVN_SCALE);
+}
+
+/*
+ * sit9531x_output_phase_flush - flush the output phase of a PLL
+ *
+ * Fires the chip's on-demand phase-flush (PHFL) so every output divider
+ * of @pll_idx restarts aligned to the PLL phase.  Without it a rewritten
+ * DIVO keeps counting from an arbitrary point and the output edge lands
+ * with a persistent offset against the tracked reference (only a power
+ * cycle realigned it).
+ *
+ * The sequence mirrors the vendor procedure: arm the on-demand PHFL and
+ * latch it with the PLL-page small-change update, then select the
+ * in-register phase trigger on Page 0 and pulse it.  The Page 0 trigger
+ * register is touched read-modify-write so the unrelated OEb bits are
+ * preserved.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_output_phase_flush(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+	u8 ctrl, orig;
+	int rc, ret;
+
+	/* Arm the on-demand phase-flush on the PLL page. */
+	rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+				    SIT9531X_PLL_REG_PHFL_CTRL,
+				    SIT9531X_PLL_PHFL_ON_DEMAND_EN,
+				    SIT9531X_PLL_PHFL_ON_DEMAND_EN);
+	if (rc)
+		return rc;
+
+	/* Latch it with the PLL small-change update. */
+	rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+				    SIT9531X_PLL_REG_SMALL_UPDATE,
+				    SIT9531X_SMALL_UPDATE_CMD,
+				    SIT9531X_SMALL_UPDATE_CMD);
+	if (rc)
+		return rc;
+
+	/*
+	 * Select the in-register phase trigger, preserving the OEb bits.
+	 * Remember the original register value (with the trigger de-asserted)
+	 * so the trigger-source select can be restored once the pulse has
+	 * fired.
+	 */
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, &ctrl);
+	if (rc)
+		return rc;
+
+	orig = ctrl & ~SIT9531X_DIVO_PHASE_TRIG;
+	ctrl = orig | SIT9531X_DIVO_PHASE_SEL_REG;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, ctrl);
+	if (rc)
+		return rc;
+
+	/*
+	 * Pulse the phase trigger.  No explicit delay is needed between the
+	 * set and clear writes: each I2C transaction takes far longer than
+	 * any minimum pulse width.
+	 */
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1,
+			       ctrl | SIT9531X_DIVO_PHASE_TRIG);
+
+	/*
+	 * Restore the original trigger-source select.  The pulse above has
+	 * already latched the flush, so a one-shot flush must not leave the
+	 * phase trigger permanently pinned to the in-register source.  This
+	 * runs even when the pulse write failed, otherwise a failed flush
+	 * would keep a hardware trigger source hijacked; the restore error
+	 * is only surfaced when it would not mask the pulse failure.
+	 */
+	ret = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, orig);
+	if (ret && !rc)
+		rc = ret;
+
+	return rc;
+}
+
+/*
+ * sit9531x_output_freq_set - set output clock frequency
+ * @out_idx:	output index (0-N for this chip variant)
+ * @pll_idx:	PLL driving this output (0-3)
+ * @frequency:	desired output frequency in Hz
+ *
+ * Computes DIVO = Fvco / frequency and writes the 34-bit output divider
+ * to the output system registers on Pages 3/4.  The write sequence is:
+ *   1. Unlock debug registers (Page 3)
+ *   2. Enter PRG_CMD state
+ *   3. Write 5-byte DIVO to the correct page/slot
+ *   4. NVM update
+ *   5. Loop lock
+ *   6. Wait for lock to settle
+ *   7. Flush the output phase so the new divider starts aligned
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, <0 on error.  Actual frequency may differ
+ *         due to integer division; the output state is updated with
+ *         the effective frequency (Fvco / DIVO).
+ */
+int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
+			     u8 pll_idx, u64 frequency)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	u8 slot, page, base_reg, divo_bytes[5], msb_old;
+	u64 fvco, divo, fvco_min, fvco_max;
+	int rc, j, ret;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (out_idx >= info->num_outputs || pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	if (!frequency)
+		return -EINVAL;
+
+	/* Determine VCO frequency band limits */
+	if (pll_idx == 1 || pll_idx == 3) {
+		/* PLLB, PLLD: high band */
+		fvco_min = SIT9531X_FVCO_HIGHBAND_MIN;
+		fvco_max = SIT9531X_FVCO_HIGHBAND_MAX;
+	} else {
+		/* PLLA, PLLC: low band */
+		fvco_min = SIT9531X_FVCO_LOWBAND_MIN;
+		fvco_max = SIT9531X_FVCO_LOWBAND_MAX;
+	}
+
+	/*
+	 * Read current VCO frequency.  When the board supplies an explicit
+	 * Fvco via "sitime,pll-fvco" the override is the source of truth
+	 * (e.g. a chip variant that runs out of the documented band, or a
+	 * mode like INTSYNC where Fref*DIVN does not reproduce the VCO), so
+	 * skip the band clamp in that case.
+	 */
+	fvco = sit9531x_get_fvco(sitdev, pll_idx);
+	if (!fvco) {
+		fvco = fvco_min;
+	} else if (!sitdev->pll_fvco[pll_idx]) {
+		if (fvco < fvco_min)
+			fvco = fvco_min;
+		else if (fvco > fvco_max)
+			fvco = fvco_max;
+	}
+
+	divo = div64_u64(fvco, frequency);
+	if (!divo)
+		return -EINVAL;
+
+	dev_dbg(sitdev->dev,
+		"out%u: Fvco=%llu freq=%llu DIVO=%llu (effective %llu Hz)\n",
+		out_idx, fvco, frequency, divo, div64_u64(fvco, divo));
+
+	/* Map output index to physical slot */
+	slot = info->clkout_map[out_idx];
+
+	/* Determine page and per-page slot register */
+	if (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX)
+		page = SIT9531X_PAGE_OUTSYS1;
+	else
+		page = SIT9531X_PAGE_OUTSYS0;
+	base_reg = clkout_odr_divn_base[slot % 6];
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
+			       SIT9531X_DEBUG_UNLOCK_VAL);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+			       SIT9531X_PRG_CMD_STATE);
+	if (rc)
+		return rc;
+
+	divo_bytes[0] = (divo >>  0) & 0xFF;
+	divo_bytes[1] = (divo >>  8) & 0xFF;
+	divo_bytes[2] = (divo >> 16) & 0xFF;
+	divo_bytes[3] = (divo >> 24) & 0xFF;
+	divo_bytes[4] = (divo >> 32) & 0x03;  /* only bits [1:0] */
+
+	rc = sit9531x_read_u8(sitdev,
+			      SIT9531X_REG(page, base_reg - 4), &msb_old);
+	if (rc)
+		goto commit;
+	divo_bytes[4] |= msb_old & 0xFC;
+
+	for (j = 0; j < 5; j++) {
+		rc = sit9531x_write_u8(sitdev,
+				       SIT9531X_REG(page, base_reg - j),
+				       divo_bytes[j]);
+		if (rc)
+			goto commit;
+	}
+
+commit:
+	/*
+	 * Step 4: NVM update + loop lock.  Always run prg_commit() so the chip
+	 * leaves the PRG_CMD state with the output loops re-locked, even when a
+	 * write above failed; keep the first error to return.  It also carries
+	 * the required post-lock settling sleep.
+	 */
+	ret = sit9531x_prg_commit(sitdev);
+	if (ret && !rc)
+		rc = ret;
+	if (rc)
+		return rc;
+
+	/*
+	 * Step 5: flush the PLL's output phase so the new DIVO starts
+	 * aligned instead of keeping the arbitrary phase the divider
+	 * happened to be at.
+	 */
+	rc = sit9531x_output_phase_flush(sitdev, pll_idx);
+	if (rc)
+		return rc;
+
+	sitdev->out[out_idx].freq = (u32)div64_u64(fvco, divo);
+
+	return 0;
+}
+
+/*
+ * sit9531x_output_freq_get - read output clock frequency from hardware
+ * @out_idx:	output index (0-N for this chip variant)
+ * @frequency:	output frequency in Hz
+ *
+ * Reads the 34-bit DIVO divider back from the output system registers
+ * and computes the live output frequency as Fvco / DIVO.  This stays
+ * correct even when the divider was reprogrammed behind the driver's
+ * back (e.g. by a direct-I2C userspace tool), where the cached value
+ * would be stale.
+ *
+ * The cached output state is refreshed with the computed value.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, -ENODEV when the output divider or VCO rate
+ *	   is not resolvable, <0 on register access error
+ */
+int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
+			     u64 *frequency)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	u8 slot, page, base_reg, pll_idx, v;
+	u64 fvco, divo = 0;
+	int rc, j;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (out_idx >= info->num_outputs)
+		return -EINVAL;
+
+	pll_idx = sitdev->out[out_idx].pll_idx;
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -ENODEV;
+
+	fvco = sit9531x_get_fvco(sitdev, pll_idx);
+	if (!fvco)
+		return -ENODEV;
+
+	slot = info->clkout_map[out_idx];
+	if (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX)
+		page = SIT9531X_PAGE_OUTSYS1;
+	else
+		page = SIT9531X_PAGE_OUTSYS0;
+	base_reg = clkout_odr_divn_base[slot % 6];
+
+	for (j = 4; j >= 0; j--) {
+		rc = sit9531x_read_u8(sitdev,
+				      SIT9531X_REG(page, base_reg - j), &v);
+		if (rc)
+			return rc;
+		if (j == 4)
+			v &= 0x03;
+		divo = (divo << 8) | v;
+	}
+
+	if (!divo)
+		return -ENODEV;
+
+	*frequency = div64_u64(fvco, divo);
+	sitdev->out[out_idx].freq = (u32)*frequency;
+
+	return 0;
+}
+
 /*
  * Phase adjust (PRG_RST_DELAY register-based).
  *
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index 67b9fbba9f2e..a7510ca721b6 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -302,6 +302,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
  *
@@ -497,6 +519,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,
@@ -561,6 +584,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,
 };
 
@@ -576,8 +600,79 @@ 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 when the
+ * output is not resolvable through the divider chain (e.g. not mapped
+ * to a PLL), so a netlink dump never fails on such pins.
+ */
+static int
+sit9531x_dpll_output_pin_frequency_get(const struct dpll_pin *pin,
+				       void *pin_priv,
+				       const struct dpll_device *dpll,
+				       void *dpll_priv, u64 *frequency,
+				       struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_output_freq_get(sitdev, dpin->id, frequency);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc)
+		*frequency = sit9531x_out_state_get(sitdev, dpin->id)->freq;
+
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_output_pin_frequency_set - set output pin frequency
+ *
+ * computes DIVO = Fvco / frequency and writes the
+ * 34-bit output divider to the output system registers via
+ * sit9531x_output_freq_set().
+ */
+static int
+sit9531x_dpll_output_pin_frequency_set(const struct dpll_pin *pin,
+				       void *pin_priv,
+				       const struct dpll_device *dpll,
+				       void *dpll_priv, u64 frequency,
+				       struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	u8 actual_pll;
+	int rc;
+
+	/*
+	 * Read the PLL that drives this output from its OUT_MAP state
+	 * (populated by out_state_fetch from the chip's OUT_MAP registers).
+	 * That is the index the output register programming below is keyed
+	 * by; the output is registered under the DPLL matching this PLL.
+	 */
+	actual_pll = sitdev->out[dpin->id].pll_idx;
+
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll,
+				      frequency);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc)
+		NL_SET_ERR_MSG(extack, "Output frequency set failed");
+
+	return rc;
+}
+
 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 dd027142b370..031c1a460d4a 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -202,6 +202,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
@@ -214,6 +218,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.43.0


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

* [PATCH net-next v8 11/15] dpll: sit9531x: add support to get and set esync on pins
  2026-09-02 21:40 [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (9 preceding siblings ...)
  2026-09-02 21:40 ` [PATCH net-next v8 10/15] dpll: sit9531x: add support to adjust output phase Ali Rouhi
@ 2026-09-02 21:40 ` Ali Rouhi
  2026-09-08 12:45   ` netdev-bot+sashiko
  2026-09-02 21:40 ` [PATCH net-next v8 13/15] dpll: sit9531x: add support to get fractional frequency offset Ali Rouhi
                   ` (4 subsequent siblings)
  15 siblings, 1 reply; 31+ messages in thread
From: Ali Rouhi @ 2026-09-02 21:40 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, 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 | 51 +++++++++++++++++++
 drivers/dpll/sit9531x/dpll.c | 99 ++++++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/regs.h |  7 +++
 3 files changed, 157 insertions(+)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index be033d7cfe7c..c93578f3b750 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -1535,6 +1535,57 @@ 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;
+	u8 slot, page, reg;
+	int rc, ret;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (out_idx >= info->num_outputs)
+		return -EINVAL;
+
+	slot = info->clkout_map[out_idx];
+	page = (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX) ?
+		SIT9531X_PAGE_OUTSYS1 : SIT9531X_PAGE_OUTSYS0;
+	reg = SIT9531X_OUT_PROG0_BASE +
+	      SIT9531X_OUT_PRG_SLOT_STRIDE * (slot % 6);
+
+	/*
+	 * PROG0 lives in the output system, so like the DIVO and
+	 * PRG_RST_DELAY writes it only takes effect inside the PRG_CMD
+	 * programming state committed to the NVM shadow.
+	 */
+	rc = sit9531x_prg_enter(sitdev);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, reg), pulse_ctrl);
+
+	/*
+	 * Always leave the PRG_CMD state via prg_commit(), even if the write
+	 * failed, so the output loops are re-locked rather than stranded
+	 * unlocked; keep the first error.
+	 */
+	ret = sit9531x_prg_commit(sitdev);
+	if (ret && !rc)
+		rc = ret;
+
+	return rc;
+}
+
 /*
  * 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 c07fb2256510..76d7d7d21d3d 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -25,6 +25,17 @@
 #define SIT9531X_ESYNC_FREQ_10MHZ	10000000ULL
 #define SIT9531X_ESYNC_PULSE_DEFAULT	50
 
+static const struct dpll_pin_frequency sit9531x_esync_ranges[] = {
+	DPLL_PIN_FREQUENCY(0),
+	DPLL_PIN_FREQUENCY(SIT9531X_ESYNC_FREQ_10MHZ),
+};
+
+static inline bool
+sit9531x_dpll_esync_pin_supported(const struct sit9531x_dpll_pin *dpin)
+{
+	return dpin->esync_control;
+}
+
 static inline bool sit9531x_dpll_is_input_pin(const struct sit9531x_dpll_pin *pin)
 {
 	return pin->dir == DPLL_PIN_DIRECTION_INPUT;
@@ -785,6 +796,92 @@ 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;
+
+	if (!sit9531x_dpll_esync_pin_supported(dpin))
+		return -EOPNOTSUPP;
+
+	esync->range = sit9531x_esync_ranges;
+	esync->range_num = ARRAY_SIZE(sit9531x_esync_ranges);
+	esync->pulse = SIT9531X_ESYNC_PULSE_DEFAULT;
+	esync->freq = dpin->esync_freq;
+
+	return 0;
+}
+
+static int
+sit9531x_dpll_output_pin_esync_set(const struct dpll_pin *pin,
+				   void *pin_priv,
+				   const struct dpll_device *dpll,
+				   void *dpll_priv,
+				   u64 freq,
+				   struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	u8 actual_pll;
+	int rc;
+
+	if (!sit9531x_dpll_esync_pin_supported(dpin)) {
+		NL_SET_ERR_MSG(extack,
+			       "Embedded sync not enabled for this pin");
+		return -EOPNOTSUPP;
+	}
+
+	actual_pll = sitdev->out[dpin->id].pll_idx;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	/*
+	 * This output is a dedicated embedded-sync pin.
+	 * Treat freq=0 as a request to disable the entire output.
+	 */
+	if (!freq) {
+		rc = sit9531x_output_disable(sitdev, dpin->id);
+		if (!rc)
+			dpin->esync_freq = 0;
+		mutex_unlock(&sitdev->multiop_lock);
+		return rc;
+	}
+
+	if (freq != SIT9531X_ESYNC_FREQ_10MHZ) {
+		mutex_unlock(&sitdev->multiop_lock);
+		NL_SET_ERR_MSG(extack,
+			       "Only 10 MHz esync frequency is supported");
+		return -EINVAL;
+	}
+
+	rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll,
+				      SIT9531X_ESYNC_FREQ_10MHZ);
+	/*
+	 * Program the pulse generator (PROG0 PULSE_CTRL) so the embedded-sync
+	 * pulse is actually emitted; without it the output carries the clock
+	 * but no esync marker.  SIT9531X_ESYNC_PULSE_DEFAULT is the same duty
+	 * the esync_get callback advertises.
+	 */
+	if (!rc)
+		rc = sit9531x_output_pulse_ctrl_set(sitdev, dpin->id,
+						    SIT9531X_ESYNC_PULSE_DEFAULT);
+	if (!rc)
+		rc = sit9531x_output_enable(sitdev, dpin->id);
+
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (!rc)
+		dpin->esync_freq = SIT9531X_ESYNC_FREQ_10MHZ;
+
+	return rc;
+}
+
 static const struct dpll_pin_ops sit9531x_dpll_output_pin_ops = {
 	.direction_get		= sit9531x_dpll_output_pin_direction_get,
 	.frequency_get		= sit9531x_dpll_output_pin_frequency_get,
@@ -793,6 +890,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/regs.h b/drivers/dpll/sit9531x/regs.h
index 91d33abd15c3..251c98dc7cbe 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -215,6 +215,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.43.0


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

* [PATCH net-next v8 10/15] dpll: sit9531x: add support to adjust output phase
  2026-09-02 21:40 [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (8 preceding siblings ...)
  2026-09-02 21:40 ` [PATCH net-next v8 08/15] dpll: sit9531x: add support to get and set frequency on pins Ali Rouhi
@ 2026-09-02 21:40 ` Ali Rouhi
  2026-09-08 12:45   ` netdev-bot+sashiko
  2026-09-02 21:40 ` [PATCH net-next v8 11/15] dpll: sit9531x: add support to get and set esync on pins Ali Rouhi
                   ` (5 subsequent siblings)
  15 siblings, 1 reply; 31+ messages in thread
From: Ali Rouhi @ 2026-09-02 21:40 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, 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 | 143 +++++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/dpll.c |  51 +++++++++++++
 drivers/dpll/sit9531x/regs.h |  26 +++++++
 3 files changed, 220 insertions(+)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 0f886cfd1401..be033d7cfe7c 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -1342,6 +1342,149 @@ 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;
+	u8 page, base, prog6_val, fine = 0;
+	u8 pll_idx, slot;
+	u32 freq;
+	int rc, ret;
+
+	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)
+		return -EINVAL;
+
+	fvco = sit9531x_get_fvco(sitdev, pll_idx);
+	if (!fvco)
+		return -EIO;
+
+	/*
+	 * Convert to unsigned absolute delay.  Negative phase (advance)
+	 * is rendered as T_out - |phase|, modulo the output period.
+	 */
+	if (phase_ps == 0) {
+		abs_ps = 0;
+	} else if (phase_ps > 0) {
+		abs_ps = (u64)phase_ps;
+	} else {
+		u64 t_out_ps = div64_u64(1000000000000ULL, freq);
+		u64 advance = (u64)(-(s64)phase_ps);
+
+		if (t_out_ps == 0)
+			return -EINVAL;
+		/*
+		 * 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);
+		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;
+
+	/* PROG6 RMW: preserve OPSTG_VCASC_BUMP in [7:5] */
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG(page, base),
+			      &prog6_val);
+	if (rc)
+		goto commit;
+
+	prog6_val &= SIT9531X_OUT_PRG_OPSTG_MASK;
+	prog6_val |= (fine << SIT9531X_OUT_PRG_FINE_SHIFT) &
+		     SIT9531X_OUT_PRG_FINE_MASK;
+	prog6_val |= (u8)((coarse >> 32) & SIT9531X_OUT_PRG_COARSE_HI_MASK);
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base),
+			       prog6_val);
+	if (rc)
+		goto commit;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base + 1),
+			       (u8)((coarse >> 24) & 0xFF));
+	if (rc)
+		goto commit;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base + 2),
+			       (u8)((coarse >> 16) & 0xFF));
+	if (rc)
+		goto commit;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base + 3),
+			       (u8)((coarse >> 8) & 0xFF));
+	if (rc)
+		goto commit;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base + 4),
+			       (u8)(coarse & 0xFF));
+
+commit:
+	/*
+	 * Always leave the PRG_CMD state via prg_commit(), even on a
+	 * mid-sequence write failure, so the output loops are re-locked rather
+	 * than stranded unlocked; keep the first error.
+	 */
+	ret = sit9531x_prg_commit(sitdev);
+	if (ret && !rc)
+		rc = ret;
+	if (rc)
+		return rc;
+
+	/*
+	 * Restart the output divider phase so the freshly programmed delay is
+	 * applied against a known edge instead of the divider's arbitrary
+	 * running phase.
+	 */
+	return sit9531x_output_phase_flush(sitdev, pll_idx);
+}
+
 /*
  * sit9531x_clear_notifications - clear all notification registers
  *
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index 6365a83e4c52..c07fb2256510 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -736,12 +736,63 @@ 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 cached value.
+ */
+static int
+sit9531x_dpll_output_pin_phase_adjust_get(const struct dpll_pin *pin,
+					  void *pin_priv,
+					  const struct dpll_device *dpll,
+					  void *dpll_priv, s32 *phase_adjust,
+					  struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+
+	*phase_adjust = dpin->phase_adjust;
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_output_pin_phase_adjust_set - set output phase adjustment
+ *
+ * Programs the per-output PRG_RST_DELAY registers for deterministic
+ * phase offset; see sit9531x_output_phase_adjust_set() in core.c.
+ */
+static int
+sit9531x_dpll_output_pin_phase_adjust_set(const struct dpll_pin *pin,
+					  void *pin_priv,
+					  const struct dpll_device *dpll,
+					  void *dpll_priv, s32 phase_adjust,
+					  struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_output_phase_adjust_set(sitdev, dpin->id, phase_adjust);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc) {
+		NL_SET_ERR_MSG(extack, "Phase adjust failed");
+		return rc;
+	}
+
+	dpin->phase_adjust = phase_adjust;
+	return 0;
+}
+
 static 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 031c1a460d4a..91d33abd15c3 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -189,6 +189,32 @@
 #define SIT9531X_REG_OUTSYS_DEBUG		SIT9531X_REG(0x03, 0xBD)
 #define SIT9531X_DEBUG_UNLOCK_VAL		0xC3
 
+/*
+ * Per-output programmable phase delay: 34-bit coarse (in VCO clock
+ * cycles) plus a 3-bit fine field with fixed 30 ps steps.  Each output
+ * has a five-byte block PROG6..PROG2:
+ *
+ *   base + 0  PROG6  [7:5] OPSTG_VCASC_BUMP (preserve via RMW)
+ *                    [4:2] PRG_RST_FINE_DELAY[2:0]
+ *                    [1:0] PRG_RST_DELAY[33:32]
+ *   base + 1  PROG5  [7:0] PRG_RST_DELAY[31:24]
+ *   base + 2  PROG4  [7:0] PRG_RST_DELAY[23:16]
+ *   base + 3  PROG3  [7:0] PRG_RST_DELAY[15:8]
+ *   base + 4  PROG2  [7:0] PRG_RST_DELAY[7:0]
+ *
+ * Outputs 0-5 are on Page 3, outputs 6-11 on Page 4.  The block base
+ * within a page is 0x15 + 16 * (out_idx % 6).
+ */
+#define SIT9531X_OUT_PRG_DELAY_BASE		0x15
+#define SIT9531X_OUT_PRG_SLOT_STRIDE		0x10
+#define SIT9531X_OUT_PRG_OPSTG_MASK		0xE0	/* bits [7:5], preserve */
+#define SIT9531X_OUT_PRG_FINE_SHIFT		2
+#define SIT9531X_OUT_PRG_FINE_MASK		0x1C	/* bits [4:2] */
+#define SIT9531X_OUT_PRG_COARSE_HI_MASK		0x03	/* bits [1:0] */
+#define SIT9531X_OUT_PRG_FINE_STEP_PS		30
+#define SIT9531X_OUT_PRG_FINE_MAX		7	/* 3-bit field */
+#define SIT9531X_OUT_PRG_COARSE_BITS		34
+
 /*
  * 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.43.0


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

* [PATCH net-next v8 13/15] dpll: sit9531x: add support to get fractional frequency offset
  2026-09-02 21:40 [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (10 preceding siblings ...)
  2026-09-02 21:40 ` [PATCH net-next v8 11/15] dpll: sit9531x: add support to get and set esync on pins Ali Rouhi
@ 2026-09-02 21:40 ` Ali Rouhi
  2026-09-08 12:45   ` netdev-bot+sashiko
  2026-09-02 21:40 ` [PATCH net-next v8 12/15] dpll: sit9531x: add support to get phase offset on the connected input pin Ali Rouhi
                   ` (3 subsequent siblings)
  15 siblings, 1 reply; 31+ messages in thread
From: Ali Rouhi @ 2026-09-02 21:40 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, 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.

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 | 151 +++++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/dpll.c |  47 +++++++++++
 drivers/dpll/sit9531x/regs.h |  12 +++
 3 files changed, 210 insertions(+)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index f552a9c73796..bd251ab60eee 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -931,6 +931,61 @@ 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, u8 *buf, unsigned int len)
+{
+	unsigned int i;
+	int rc;
+	u8 v;
+
+	if (len > SIT9531X_DBG_DATA_BYTES)
+		return -EINVAL;
+
+	rc = sit9531x_write_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_DEBUG,
+				   SIT9531X_PLL_DEBUG_UNLOCK);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+				   SIT9531X_PLL_REG_DBG_READ_CODE, read_code);
+	if (rc)
+		return rc;
+
+	/*
+	 * Reading the trigger latches a sample of the selected tap.  Read it
+	 * three times, as the vendor 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
@@ -998,6 +1053,102 @@ 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.
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int sit9531x_divn_runtime(struct sit9531x_dev *sitdev, u8 pll_idx,
+				 s64 *divn)
+{
+	u8 buf[SIT9531X_DBG_DATA_BYTES];
+	u64 fracn_raw = 0, fracd = 0;
+	u32 int_part;
+	int rc, i;
+
+	rc = sit9531x_dbg_sample(sitdev, pll_idx, SIT9531X_DBG_READ_CODE_DIVN,
+				 buf, SIT9531X_DBG_DATA_BYTES);
+	if (rc)
+		return rc;
+
+	for (i = 5; i >= 0; i--)
+		fracn_raw = (fracn_raw << 8) | buf[i];
+
+	int_part = buf[6] | ((u32)(buf[7] & SIT9531X_DIVN_RT_INT_HI_BIT) << 8);
+
+	rc = sit9531x_dbg_sample(sitdev, pll_idx,
+				 SIT9531X_DBG_READ_CODE_DIVN_DEN, buf,
+				 SIT9531X_DBG_DATA_BYTES);
+	if (rc)
+		return rc;
+
+	for (i = 5; i >= 0; i--)
+		fracd = (fracd << 8) | buf[i];
+
+	fracd |= (u64)(buf[6] & SIT9531X_DIVN_RT_INT_HI_BIT) << 48;
+
+	*divn = sit9531x_divn_fixed(int_part,
+				    sign_extend64(fracn_raw,
+						  SIT9531X_DIVN_RT_NUM_BITS - 1),
+				    fracd);
+
+	return 0;
+}
+
+/**
+ * sit9531x_pll_ffo_ppt - fractional frequency offset of a PLL's reference
+ * @sitdev:	device pointer
+ * @pll_idx:	PLL index (0-3)
+ * @ffo:	result in parts per trillion
+ *
+ * A locked PLL commands whatever DIVN keeps its VCO tracking the
+ * reference.  How far that sits from the configured DIVN is how far the
+ * reference sits from the local oscillator, which is the fractional
+ * frequency offset the DPLL ABI reports for the pin feeding the device.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, -ENODATA when DIVN is not programmed, <0 on
+ * error.
+ */
+int sit9531x_pll_ffo_ppt(struct sit9531x_dev *sitdev, u8 pll_idx, s64 *ffo)
+{
+	s64 configured, running, delta;
+	u64 magnitude;
+	int rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	rc = sit9531x_divn_static(sitdev, pll_idx, &configured);
+	if (rc)
+		return rc;
+	if (configured <= 0)
+		return -ENODATA;
+
+	rc = sit9531x_divn_runtime(sitdev, pll_idx, &running);
+	if (rc)
+		return rc;
+
+	delta = running - configured;
+	magnitude = mul_u64_u64_div_u64(abs(delta), SIT9531X_PPT_PER_UNIT,
+					(u64)configured);
+
+	*ffo = delta < 0 ? -(s64)magnitude : (s64)magnitude;
+
+	return 0;
+}
+
 /*
  * sit9531x_get_fvco - read VCO frequency from chip's DIVN registers
  *
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index f813126077e1..1a14255e89a8 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -528,6 +528,52 @@ sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
 	return 0;
 }
 
+/*
+ * sit9531x_dpll_input_pin_phase_offset_get - read phase offset
+ *
+ * reads the TDC (Time-to-Digital Converter) hardware
+ * to measure the phase difference in picoseconds via
+ * sit9531x_phase_offset_read().
+ */
+/*
+ * sit9531x_dpll_input_pin_ffo_get - read the input's frequency offset
+ *
+ * The offset is derived from how far the PLL's running DIVN sits from
+ * its configured one, which only says something about the reference the
+ * PLL is actually tracking.  For every other input there is no
+ * measurement, and -ENODATA leaves the attribute out rather than
+ * reporting the active reference's figure against the wrong pin.
+ */
+static int
+sit9531x_dpll_input_pin_ffo_get(const struct dpll_pin *pin, void *pin_priv,
+				const struct dpll_device *dpll, void *dpll_priv,
+				struct dpll_ffo_param *ffo,
+				struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	/*
+	 * The periodic worker updates selected_ref under the same lock, so
+	 * test it here rather than before taking it: otherwise the
+	 * reference can change in between and the measurement gets
+	 * attributed to the wrong pin.
+	 */
+	if (sitdev->chan[sitdpll->id].selected_ref != dpin->id) {
+		mutex_unlock(&sitdev->multiop_lock);
+		return -ENODATA;
+	}
+
+	rc = sit9531x_pll_ffo_ppt(sitdev, sitdpll->id, &ffo->ffo);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	return rc;
+}
+
 /*
  * sit9531x_dpll_input_pin_phase_offset_get - phase offset of a reference
  *
@@ -641,6 +687,7 @@ static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
 	 * 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 aa60be243e6d..98425c04d3d0 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -292,6 +292,18 @@
 #define SIT9531X_DBG_READ_CODE_TDC		69
 #define SIT9531X_TDC_SIGN_BIT		3
 
+/*
+ * Read codes of the running DIVN taps.  Unlike the configuration
+ * registers these report what the digital loop currently commands, so
+ * they carry the correction the loop applies to track its reference.
+ * The integer part and the numerator share one tap, the denominator
+ * has its own.
+ */
+#define SIT9531X_DBG_READ_CODE_DIVN		0x57
+#define SIT9531X_DBG_READ_CODE_DIVN_DEN	0x56
+#define SIT9531X_DIVN_RT_NUM_BITS		48
+#define SIT9531X_DIVN_RT_INT_HI_BIT		BIT(0)
+
 /*
  * DIVN carried as fixed point, and the unit the DPLL ABI wants the
  * fractional frequency offset in.  Equal in value, distinct in meaning.
-- 
2.43.0


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

* [PATCH net-next v8 12/15] dpll: sit9531x: add support to get phase offset on the connected input pin
  2026-09-02 21:40 [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (11 preceding siblings ...)
  2026-09-02 21:40 ` [PATCH net-next v8 13/15] dpll: sit9531x: add support to get fractional frequency offset Ali Rouhi
@ 2026-09-02 21:40 ` Ali Rouhi
  2026-09-08 12:45   ` netdev-bot+sashiko
  2026-09-02 21:40 ` [PATCH net-next v8 15/15] dpll: sit9531x: allow the device tree to override two board facts Ali Rouhi
                   ` (2 subsequent siblings)
  15 siblings, 1 reply; 31+ messages in thread
From: Ali Rouhi @ 2026-09-02 21:40 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, 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 | 125 +++++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/dpll.c | 100 ++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/regs.h |  36 ++++++++++
 3 files changed, 261 insertions(+)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index c93578f3b750..f552a9c73796 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -1586,6 +1586,131 @@ 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) 40-bit code from the
+ * PLL page registers, then converts to picoseconds using the VCO
+ * frequency: phase_diff = tdc_code / fvco.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_phase_offset_read(struct sit9531x_dev *sitdev, u8 pll_idx,
+			       s64 *phase_ps)
+{
+	u64 fvco, fvco_mhz;
+	s64 tdc_signed;
+	u64 tdc_raw;
+	int rc, i;
+	bool sign;
+	u8 v;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	/* Unlock the debug page so the TDC registers are accessible. */
+	rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+				   SIT9531X_PLL_REG_DEBUG,
+				   SIT9531X_PLL_DEBUG_UNLOCK);
+	if (rc)
+		return rc;
+
+	/*
+	 * Select the debug clock for taps below 200 kHz, then point the
+	 * readback at the TDC.  Only the one bit is touched: writing the
+	 * modifier register whole would clear the fields belonging to
+	 * other taps.
+	 */
+	rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+				    SIT9531X_PLL_REG_DBG_WRITE_CODE,
+				    SIT9531X_DBG_LOW_FREQ_CLK_BIT,
+				    SIT9531X_DBG_LOW_FREQ_CLK_BIT);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+				   SIT9531X_PLL_REG_DBG_READ_CODE,
+				   SIT9531X_DBG_READ_CODE_TDC);
+	if (rc)
+		return rc;
+
+	/* Latch a sample by reading the trigger register; see dbg_sample(). */
+	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;
+	}
+
+	tdc_raw = 0;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_4, &v);
+	if (rc)
+		return rc;
+	sign = !!(v & BIT(SIT9531X_TDC_SIGN_BIT));
+	tdc_raw = (u64)(v & 0x07) << 32;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_3, &v);
+	if (rc)
+		return rc;
+	tdc_raw |= (u64)v << 24;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_2, &v);
+	if (rc)
+		return rc;
+	tdc_raw |= (u64)v << 16;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_1, &v);
+	if (rc)
+		return rc;
+	tdc_raw |= (u64)v << 8;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_0, &v);
+	if (rc)
+		return rc;
+	tdc_raw |= v;
+
+	/*
+	 * Apply sign.  Per the vendor reference the sign bit is active-high
+	 * for a positive offset: bit set -> +code, bit clear -> -code.
+	 */
+	tdc_signed = sign ? (s64)tdc_raw : -(s64)tdc_raw;
+
+	/*
+	 * Get VCO frequency for conversion.  Fvco==0 means DIVN is not
+	 * programmed (PLL unused on this board) -- skip silently rather
+	 * than spamming the log on every poll cycle.
+	 */
+	fvco = sit9531x_get_fvco(sitdev, pll_idx);
+	if (!fvco) {
+		dev_dbg(sitdev->dev, "PLL%c: Fvco unknown, skip TDC\n",
+			'A' + pll_idx);
+		return -ENODEV;
+	}
+
+	/*
+	 * phase_diff (seconds) = tdc_code / fvco
+	 * phase_diff (ps) = tdc_code * 1e12 / fvco
+	 *
+	 * To avoid 64-bit overflow:
+	 *   phase_ps = tdc_code * 1e6 / (fvco / 1e6)
+	 */
+	fvco_mhz = div64_u64(fvco, 1000000ULL);
+	if (!fvco_mhz)
+		return -EIO;
+
+	*phase_ps = div64_s64(tdc_signed * 1000000LL, (s64)fvco_mhz);
+
+	return 0;
+}
+
 /*
  * sit9531x_ref_state_fetch - read input reference status from hardware
  * @index:	logical input index
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index 76d7d7d21d3d..f813126077e1 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -528,6 +528,105 @@ 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 vendor's 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 behind a debugfs operation 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;
+	s64 offset;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	/*
+	 * The on-chip TDC is a per-PLL resource that always measures the
+	 * phase difference between the VCO and the PLL's currently
+	 * selected reference; it cannot be pointed at an arbitrary input.
+	 * For any input that is not the active reference there is no
+	 * meaningful per-pin phase offset, so report 0 instead of the
+	 * active reference's value.
+	 */
+	if (sitdev->chan[sitdpll->id].selected_ref != dpin->id) {
+		mutex_unlock(&sitdev->multiop_lock);
+		dpin->phase_offset = 0;
+		*phase_offset = 0;
+		return 0;
+	}
+
+	rc = sit9531x_phase_offset_read(sitdev, sitdpll->id, &offset);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	/*
+	 * -ENODEV means the PLL has no programmed DIVN (unused on this
+	 * board); report phase_offset = 0 so a full pin-get dump does not
+	 * fail just because one DPLL is dormant.
+	 */
+	if (rc == -ENODEV) {
+		dpin->phase_offset = 0;
+		*phase_offset = 0;
+		return 0;
+	}
+	if (rc) {
+		NL_SET_ERR_MSG(extack, "TDC phase readback failed");
+		return rc;
+	}
+
+	/*
+	 * The ABI reports phase offset in units of 1/DPLL_PHASE_OFFSET_DIVIDER
+	 * picoseconds: the integer part of the attribute is the value divided
+	 * by the divider, the remainder is the fraction.  The TDC resolves one
+	 * VCO period (hundreds of picoseconds), so the fractional digits are
+	 * always zero here, but the magnitude still has to be scaled or every
+	 * reading would be reported a thousand times too small.
+	 */
+	offset *= DPLL_PHASE_OFFSET_DIVIDER;
+
+	dpin->phase_offset = offset;
+	*phase_offset = offset;
+	return 0;
+}
+
 static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
 	.direction_get		= sit9531x_dpll_input_pin_direction_get,
 	.frequency_get		= sit9531x_dpll_input_pin_frequency_get,
@@ -535,6 +634,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,
 	/*
 	 * The measurement compares the PLL's running feedback divider with
 	 * its configured one, so it describes the device's own reference
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index 251c98dc7cbe..aa60be243e6d 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -256,6 +256,42 @@
 #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
+
+/*
+ * Signal pathway debug readback -- PLL page.  Dig_Sys_ReadCode selects
+ * which point of the pathway is tapped, Dig_Sys_WriteCode carries the
+ * modifiers for that read, Dig_Sys_read7..read0 hold the sampled bytes
+ * and the trigger register latches a sample.  The TDC phase
+ * measurement is one tap among several, reached through read code 69.
+ */
+#define SIT9531X_PLL_REG_DBG_READ_CODE	0xB3
+#define SIT9531X_PLL_REG_DBG_WRITE_CODE	0xB4
+#define SIT9531X_DBG_LOW_FREQ_CLK_BIT	BIT(7)
+#define SIT9531X_PLL_REG_DBG_DATA_0		0xB5  /* [7:0] */
+#define SIT9531X_PLL_REG_DBG_DATA_1		0xB6  /* [15:8] */
+#define SIT9531X_PLL_REG_DBG_DATA_2		0xB7  /* [23:16] */
+#define SIT9531X_PLL_REG_DBG_DATA_3		0xB8  /* [31:24] */
+#define SIT9531X_PLL_REG_DBG_DATA_4		0xB9  /* [39:32] + sign */
+#define SIT9531X_PLL_REG_DBG_DATA_5		0xBA  /* [47:40] */
+#define SIT9531X_PLL_REG_DBG_DATA_6		0xBB
+#define SIT9531X_PLL_REG_DBG_DATA_7		0xBC
+#define SIT9531X_PLL_REG_DBG_TRIGGER		0xD0  /* read to latch a sample */
+
+/*
+ * Reads of the trigger needed to latch a fresh sample.  One returns the
+ * previous latch, which the vendor 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
+
 /*
  * DIVN carried as fixed point, and the unit the DPLL ABI wants the
  * fractional frequency offset in.  Equal in value, distinct in meaning.
-- 
2.43.0


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

* [PATCH net-next v8 14/15] dpll: sit9531x: model the inter-PLL sync net as a pair of pins
  2026-09-02 21:40 [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (13 preceding siblings ...)
  2026-09-02 21:40 ` [PATCH net-next v8 15/15] dpll: sit9531x: allow the device tree to override two board facts Ali Rouhi
@ 2026-09-02 21:40 ` Ali Rouhi
  2026-09-08 12:45   ` netdev-bot+sashiko
  2026-09-08 12:57 ` [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Paolo Abeni
  15 siblings, 1 reply; 31+ messages in thread
From: Ali Rouhi @ 2026-09-02 21:40 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, 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 | 154 ++++++++++++++++++++++++
 drivers/dpll/sit9531x/dpll.c | 227 ++++++++++++++++++++++++++++++++++-
 drivers/dpll/sit9531x/regs.h |   3 +
 3 files changed, 383 insertions(+), 1 deletion(-)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index bd251ab60eee..c3d7c4851549 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -1686,6 +1686,160 @@ 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 },
+};
+
+/*
+ * sit9531x_intsync_enable - enable inter-PLL synchronization
+ * @src_pll_idx: source (frequency) PLL index (0-3)
+ *
+ * Enables INTSYNC global bit, unlocks the source PLL's EXT page
+ * debug registers, writes configuration, and triggers a small
+ * update on the source PLL.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_intsync_enable(struct sit9531x_dev *sitdev, u8 src_pll_idx)
+{
+	u8 ext_page, val;
+	int rc, i;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (src_pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	ext_page = SIT9531X_PLL_EXT_PAGE(src_pll_idx);
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &val);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL,
+			       val | BIT(SIT9531X_INTSYNC_EN_BIT));
+	if (rc)
+		return rc;
+
+	/* Small update on Page 0 */
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+			       SIT9531X_SMALL_UPDATE_CMD);
+	if (rc)
+		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.
+	 */
+	sit9531x_intsync_disable(sitdev, src_pll_idx);
+
+	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);
+	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)
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index 1a14255e89a8..993a991d5b25 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -41,6 +41,20 @@ static inline bool sit9531x_dpll_is_input_pin(const struct sit9531x_dpll_pin *pi
 	return pin->dir == DPLL_PIN_DIRECTION_INPUT;
 }
 
+static inline bool
+sit9531x_dpll_is_intsync_pin(const struct sit9531x_dpll_pin *pin)
+{
+	return sit9531x_dpll_is_input_pin(pin) &&
+	       pin->id == SIT9531X_INTSYNC_PIN_ID;
+}
+
+static inline bool
+sit9531x_dpll_is_intsync_src_pin(const struct sit9531x_dpll_pin *pin)
+{
+	return !sit9531x_dpll_is_input_pin(pin) &&
+	       pin->id == SIT9531X_INTSYNC_OUT_PIN_ID;
+}
+
 static inline bool
 sit9531x_dpll_is_xo_pin(const struct sit9531x_dpll_pin *pin)
 {
@@ -717,8 +731,214 @@ 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;
+	u8 prio;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	switch (state) {
+	case DPLL_PIN_STATE_CONNECTED:
+		if (sitdev->intsync_src == sitdpll->id)
+			break;
+		if (sitdev->intsync_src >= 0) {
+			NL_SET_ERR_MSG(extack,
+				       "INTSYNC is already sourced by another PLL");
+			rc = -EBUSY;
+			break;
+		}
+		/*
+		 * A PLL that already lists INTSYNC among its references must
+		 * not also drive it: the destination side refuses the mirror
+		 * of this, and without the check here the net could be routed
+		 * back into the PLL feeding it.
+		 */
+		rc = sit9531x_input_prio_get(sitdev, sitdpll->id,
+					     sit9531x_input_hw_src(SIT9531X_INTSYNC_PIN_ID),
+					     &prio);
+		if (rc)
+			break;
+		if (prio < SIT9531X_PRIO_MAX_SLOTS) {
+			NL_SET_ERR_MSG(extack,
+				       "PLL selects INTSYNC as a reference; it cannot drive it");
+			rc = -EBUSY;
+			break;
+		}
+		rc = sit9531x_intsync_enable(sitdev, sitdpll->id);
+		if (!rc)
+			sitdev->intsync_src = sitdpll->id;
+		break;
+	case DPLL_PIN_STATE_DISCONNECTED:
+		if (sitdev->intsync_src != sitdpll->id)
+			break;
+		rc = sit9531x_intsync_disable(sitdev, sitdpll->id);
+		if (!rc)
+			sitdev->intsync_src = -1;
+		break;
+	default:
+		rc = -EINVAL;
+		break;
+	}
+
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc && rc != -EBUSY)
+		NL_SET_ERR_MSG(extack, "Failed to set INTSYNC source state");
+
+	return rc;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_intsync_src_pin_ops = {
+	.direction_get		= sit9531x_dpll_output_pin_direction_get,
+	.state_on_dpll_get	= sit9531x_dpll_intsync_src_state_on_dpll_get,
+	.state_on_dpll_set	= sit9531x_dpll_intsync_src_state_on_dpll_set,
+};
+
 /* ---- INTSYNC destination (input) pin ---- */
 
+/*
+ * sit9531x_dpll_intsync_dst_state_on_dpll_get - INTSYNC reference state
+ *
+ * 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
  *
@@ -1044,8 +1264,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 98425c04d3d0..1db6dcf342e5 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -317,6 +317,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.43.0


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

* [PATCH net-next v8 15/15] dpll: sit9531x: allow the device tree to override two board facts
  2026-09-02 21:40 [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (12 preceding siblings ...)
  2026-09-02 21:40 ` [PATCH net-next v8 12/15] dpll: sit9531x: add support to get phase offset on the connected input pin Ali Rouhi
@ 2026-09-02 21:40 ` Ali Rouhi
  2026-09-08 12:45   ` netdev-bot+sashiko
  2026-09-02 21:40 ` [PATCH net-next v8 14/15] dpll: sit9531x: model the inter-PLL sync net as a pair of pins Ali Rouhi
  2026-09-08 12:57 ` [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Paolo Abeni
  15 siblings, 1 reply; 31+ messages in thread
From: Ali Rouhi @ 2026-09-02 21:40 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, 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 | 39 ++++++++++++++++++++++++++++++++++++
 1 file changed, 39 insertions(+)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index c3d7c4851549..d75ac02047b5 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -3158,6 +3158,43 @@ static u64 sit9531x_derive_clock_id(struct sit9531x_dev *sitdev)
 	return clkid;
 }
 
+/*
+ * Board-config overrides for fixed efuse/blob routing the chip registers do
+ * not describe unambiguously.  Absent properties leave pll_fvco[] zeroed
+ * (derive from DIVN) and out_pll_map_valid false (use the OUT_MAP registers).
+ */
+static void sit9531x_parse_board_config(struct sit9531x_dev *sitdev)
+{
+	u32 map[SIT9531X_MAX_OUTPUTS];
+	int n, i;
+
+	device_property_read_u64_array(sitdev->dev, "sitime,pll-fvco",
+				       sitdev->pll_fvco, SIT9531X_NUM_PLLS);
+
+	if (!device_property_present(sitdev->dev, "sitime,output-pll-map"))
+		return;
+
+	/*
+	 * Any 1..MAX_OUTPUTS length is accepted so the 8-output SiT95317 need
+	 * not pad to 12; variant detection has not run yet and entries past
+	 * the detected num_outputs are never indexed.  Trailing entries of a
+	 * short map must read as unmapped rather than 0 (== PLLA), which
+	 * would mark unrouted outputs active in sit9531x_out_state_fetch().
+	 */
+	memset(sitdev->out_pll_map, SIT9531X_OUT_PLL_UNMAPPED,
+	       sizeof(sitdev->out_pll_map));
+
+	n = device_property_count_u32(sitdev->dev, "sitime,output-pll-map");
+	if (n <= 0 || n > SIT9531X_MAX_OUTPUTS ||
+	    device_property_read_u32_array(sitdev->dev, "sitime,output-pll-map",
+					   map, n))
+		return;
+
+	for (i = 0; i < n; i++)
+		sitdev->out_pll_map[i] = map[i];
+	sitdev->out_pll_map_valid = true;
+}
+
 int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
 {
 	struct clk *xtal_clk;
@@ -3192,6 +3229,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.43.0


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

* Re: [PATCH net-next v8 03/15] dpll: add basic SiTime SiT9531x support
  2026-09-02 21:40 ` [PATCH net-next v8 03/15] dpll: add basic SiTime SiT9531x support Ali Rouhi
@ 2026-09-08 12:45   ` netdev-bot+sashiko
  0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-09-08 12:45 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel, kuba

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

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

- [Low] Orphan kernel-doc-style block for `sit9531x_input_get_regs()` in
  drivers/dpll/sit9531x/core.c (lines 147-155) documents a function that…
- [Low] The Kconfig help for SIT9531X_DPLL advertises DPLL device
  registration, DPLL pin exposure and Generic Netlink configuration, and…

--

dpll: add basic SiTime SiT9531x support

This adds the initial SiTime SiT9531x I2C clock generator driver: a paged
regmap configuration for the 32x256 register space selected through offset
0xFF, page/offset register access helpers, and a probe that fetches the
xtal rate, takes the optional reset line and identifies the chip variant.

Two small observations below, both cosmetic.

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

[Severity: Low]
This isn't a bug, but the help text describes behaviour that only arrives
later in the series.  At this commit sit9531x_dev_probe() stops after
identifying the variant:

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

	return 0;

There is no dpll_device_register() or dpll_pin_register() call yet, and
select DPLL has no consumer, so enabling CONFIG_SIT9531X_DPLL at this point
in the series binds the I2C node without exposing any DPLL object.

The later patches in this series do add the DPLL device and pin
registration, so the help text matches the final state of the series;
should the Kconfig entry land with the patch that implements it instead?

[ ... ]

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> new file mode 100644
> index 000000000000..f355b08aebca
> --- /dev/null
> +++ b/drivers/dpll/sit9531x/core.c

[ ... ]

> +/*
> + * 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 int sit9531x_read_variant_id(struct sit9531x_dev *sitdev, u8 *id)
> +{
> +	return sit9531x_read_u8(sitdev, SIT9531X_REG_VARIANT_ID, id);
> +}

[Severity: Low]
This isn't a bug, but the comment block for sit9531x_input_get_regs()
describes a function that does not exist in this patch, and it sits
directly above the unrelated sit9531x_read_variant_id().  A reader here
would attribute the Page 0x02 register-pair semantics to the variant-ID
helper.

The function it documents is added later in the series, by "dpll:
sit9531x: implement input pin state on a DPLL", where it appears right
under this same comment.  Could the comment move to that patch?

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

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

* Re: [PATCH net-next v8 02/15] dt-bindings: dpll: add SiTime SiT95316 clock generator
  2026-09-02 21:40 ` [PATCH net-next v8 02/15] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
@ 2026-09-08 12:45   ` netdev-bot+sashiko
  0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-09-08 12:45 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, 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: 0 · Medium: 1 · Low: 2

- [Medium] New DT property `sitime,pll-fvco`
  (Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml:51) omits…
- [Low] Concern (assessed invalid): the MAINTAINERS section "SITIME
  SIT9531X DPLL DRIVER" added at MAINTAINERS:25160 lists only the…
- [Low] Concern (assessed invalid): the schema does not bound DPLL
  channel count (`dpll-types` has no maxItems: 4) nor per-variant…

--

dt-bindings: dpll: add SiTime SiT95316 clock generator

Adds a new DT binding for the SiTime SiT95316/SiT95317 I2C clock
generators, described as a DPLL provider with four PLLs, eight
selectable inputs and up to twelve outputs, plus two vendor properties
(sitime,pll-fvco and sitime,output-pll-map).  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 000000000000..b3cf9d5a8267
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
> @@ -0,0 +1,171 @@

[ ... ]

> +  sitime,pll-fvco:
> +    $ref: /schemas/types.yaml#/definitions/uint64-array
> +    minItems: 4
> +    maxItems: 4
> +    description:
> +      Per-PLL VCO frequency in Hz for PLLA, PLLB, PLLC, PLLD.  Neither
> +      standard frequency suffix fits.  The VCO bands are 4915.2 MHz to
> +      5898.24 MHz and 6875 MHz to 7812.5 MHz, so every valid value
> +      exceeds the 32 bits "-hz" allows, and the band edges are not
> +      whole megahertz either, so "-mhz" cannot express them.  Hence a
> +      plain uint64-array.  Override where the standard

[Severity: Medium]
Is the statement that "every valid value exceeds the 32 bits "-hz" allows"
correct?  A "-hz" property can carry 64-bit values, so the stated reason for
dropping the unit suffix does not seem to hold.

The peer binding in the same directory does exactly that:

Documentation/devicetree/bindings/dpll/microchip,zl30731.yaml
            supported-frequencies-hz = /bits/ 64 <10000 10000000>;

And this very file does it too, in its own examples:

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

Documentation/devicetree/bindings/opp/opp-v2-base.yaml also documents this
explicitly:

      opp-hz:
        description:
          Frequency in Hz, expressed as a 64-bit big-endian integer.

Since the values are already in Hz, could the property be named
sitime,pll-fvco-hz while keeping

    $ref: /schemas/types.yaml#/definitions/uint64-array

and the 32-bit rationale dropped from the description?  Once merged, the
suffix-less spelling becomes DT ABI and cannot be renamed later.

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

[ ... ]

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

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

* Re: [PATCH net-next v8 04/15] dpll: sit9531x: read DPLL types and pin properties from system firmware
  2026-09-02 21:40 ` [PATCH net-next v8 04/15] dpll: sit9531x: read DPLL types and pin properties from system firmware Ali Rouhi
@ 2026-09-08 12:45   ` netdev-bot+sashiko
  0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-09-08 12:45 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, 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: 1 · Low: 5

- [Medium] sit9531x_pin_props_get() fabricates the pin's advertised
  frequency capability set instead of deriving it from firmware: after…
- [Low] Concern (largely INVALID at the end of the series): the per-pin
  'hardware state' (sitdev->ref[].freq / sitdev->out[].freq and the…
- [Low] sit9531x_pin_props_get() accepts two 'connection-type' spellings
  that the device-tree binding forbids: it compares the property against…
- [Low] Concern (verified as practically unreachable): supported-
  frequencies-hz is a 64-bit DT property and dpll_pin_frequency.min/max…
- [Low] The XO (crystal) input pin has two conflicting sources of truth
  for its frequency.
- [Low] sit9531x_pin_props_get() ignores the return value of
  fwnode_property_read_u64_array() for "supported-frequencies-hz": it…

--

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

This patch adds drivers/dpll/sit9531x/prop.c, which builds the per-pin
dpll_pin_properties (package label, type, capabilities, supported
frequency list) and the per-channel dpll_type from the firmware node,
falling back to defaults when the node does not describe them.  A few
questions below about the frequency list and the connection-type parsing.

> diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
> new file mode 100644
> index 000000000000..94f3da716297
> --- /dev/null
> +++ b/drivers/dpll/sit9531x/prop.c
> @@ -0,0 +1,406 @@

[ ... ]

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

[Severity: Low]
Does the XO pin end up with two sources of truth for its frequency here?

The branch seeds sitdev->ref[SIT9531X_MAX_INPUTS].freq from
sitdev->xtal_freq (the clk_get_rate() value read at probe), but unlike the
two INTSYNC pins the XO index is not excluded from the firmware-node path
further down in sit9531x_pin_props_get().

dpll-device.yaml allows an input-pins child for this index:

    patternProperties:
      "^pin@[0-9a-f]+$":
        $ref: /schemas/dpll/dpll-pin.yaml

so a schema-valid DT can supply supported-frequencies-hz for pin 8, and the
seeding block later overwrites ref[8].freq (and curr_freq) with freqs[0].
>From then on sit9531x_dpll_input_pin_frequency_get() reports one crystal
rate to userspace while all the divider and VCO math keeps using
sitdev->xtal_freq.

Would it be simpler to skip the frequency-seeding path for index
SIT9531X_MAX_INPUTS the same way both INTSYNC pins already are?

[ ... ]

> +	/* Look for "connection-type" property -> pin type enum */
> +	if (!fwnode_property_read_string(props->fwnode, "connection-type",
> +					 &type)) {
> +		if (!strcmp(type, "ext"))
> +			props->dpll_props.type = DPLL_PIN_TYPE_EXT;
> +		else if (!strcmp(type, "gnss"))
> +			props->dpll_props.type = DPLL_PIN_TYPE_GNSS;
> +		else if (!strcmp(type, "int") ||
> +			 !strcmp(type, "int-oscillator"))
> +			props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
> +		else if (!strcmp(type, "synce") ||
> +			 !strcmp(type, "synce-eth-port"))
> +			props->dpll_props.type = DPLL_PIN_TYPE_SYNCE_ETH_PORT;

[Severity: Low]
Can the "int-oscillator" and "synce-eth-port" comparisons ever match?

Documentation/devicetree/bindings/dpll/dpll-pin.yaml restricts the property
to a closed set:

  connection-type:
    description: Connection type of the pin
    $ref: /schemas/types.yaml#/definitions/string
    enum: [ext, gnss, int, mux, synce]

and sitime,sit95316.yaml inherits that pin schema through dpll-device.yaml.
A DT using either alias fails dt_binding_check/dtbs_check, so those two
comparisons look like dead code for any schema-valid DT.  Should they just
be dropped, or should the binding gain the aliases?

> +	num_freqs = fwnode_property_count_u64(props->fwnode,
> +					      "supported-frequencies-hz");
> +	if (num_freqs <= 0) {
> +		num_freqs = 0;
> +		goto skip_fwnode_props;
> +	}
> +
> +	freqs = kcalloc(num_freqs, sizeof(*freqs), GFP_KERNEL);
> +	if (!freqs) {
> +		rc = -ENOMEM;
> +		goto err_alloc_freqs;
> +	}
> +
> +	fwnode_property_read_u64_array(props->fwnode,
> +				       "supported-frequencies-hz",
> +				       freqs, num_freqs);

[Severity: Low]
Should the return value of fwnode_property_read_u64_array() be checked
before freqs[] is consumed?

For device tree the element count and the array read parse the same
property length and cannot disagree.  For an ACPI-described device (this
driver can still be instantiated through PRP0001, which matches on
of_match_table and takes properties from _DSD), acpi_data_prop_read()
returns the package element count without type-checking each element, while
acpi_copy_property_array_uint() fails with -EPROTO on the first
non-integer element.

Since the failure is discarded, the remaining kcalloc zeros are then used
as data: ref[index].freq / out[index].freq and curr_freq get freqs[0], and
every entry including the zeros is published as a supported range.

> +	/*
> +	 * Seed the runtime ref->freq / out->freq with the first DT-listed
> +	 * supported frequency so the netlink frequency_get callback reports
> +	 * a sane initial value before any pin_set occurs.  DT lists the
> +	 * physically-wired reference frequency for each input pin and the
> +	 * default output frequency for each output pin.
> +	 */
> +	if (num_freqs > 0) {
> +		if (dir == DPLL_PIN_DIRECTION_INPUT)
> +			sitdev->ref[index].freq = (u32)freqs[0];
> +		else
> +			sitdev->out[index].freq = (u32)freqs[0];
> +		curr_freq = (u32)freqs[0];
> +	}
> +
> +skip_fwnode_props:

[ ... ]

> +	/* Allocate frequency ranges list -- DT discrete entries + current
> +	 * freq + one catch-all wide range so the subsystem never pre-
> +	 * rejects a frequency_set call.  The chip's real admissible set
> +	 * is bounded by VCO / divider math in sit9531x_output_freq_set().
> +	 */
> +	ranges = kcalloc(num_freqs + 2, sizeof(*ranges), GFP_KERNEL);
> +	if (!ranges) {
> +		rc = -ENOMEM;
> +		goto err_alloc_ranges;
> +	}
> +
> +	/* Current freq as first entry */
> +	ranges[0] = (struct dpll_pin_frequency)DPLL_PIN_FREQUENCY(curr_freq);
> +	j = 1;

[ ... ]

> +	/* Always append a wide catch-all range */
> +	ranges[j].min = 1;
> +	ranges[j].max = 1000000000ULL; /* 1 GHz */
> +	j++;
> +
> +	props->dpll_props.freq_supported = ranges;
> +	props->dpll_props.freq_supported_num = j;

[Severity: Medium]
Does the appended catch-all range make the firmware-declared frequency list
meaningless, and does it publish a capability set the driver cannot honour?

The core treats every advertised range as genuinely supported, both when
validating a set and when exporting the list to userspace:

drivers/dpll/dpll_netlink.c:dpll_pin_is_freq_supported() {
	for (fs = 0; fs < pin->prop.freq_supported_num; fs++)
		if (freq >= pin->prop.freq_supported[fs].min &&
		    freq <= pin->prop.freq_supported[fs].max)
			return true;
	return false;
}

and it is the only generic pre-validation of a userspace request:

drivers/dpll/dpll_netlink.c:dpll_pin_freq_set() {
	if (!dpll_pin_is_freq_supported(pin, freq)) {
		NL_SET_ERR_MSG_ATTR(extack, a, "frequency is not supported by the device");
		return -EINVAL;
	}

With {1, 1000000000} counted in freq_supported_num for every non-INTSYNC
pin, the DT-listed entries never constrain anything, and the same range is
attached to the XO pin whose capabilities are deliberately 0 and to every
input pin, which has no .frequency_set at all.

The array is also copied into the pin by dpll_pin_prop_dup() and exported
through DPLL_A_PIN_FREQUENCY_SUPPORTED, so 1 Hz..1 GHz becomes what
userspace is told this device supports.

Separately, ranges[0] is always DPLL_PIN_FREQUENCY(curr_freq), and
curr_freq stays 0 for any pin with no firmware node or no
supported-frequencies-hz list (both permitted by dpll-device.yaml and
dpll-pin.yaml), so those pins advertise 0 Hz as a supported frequency.

The commit message says:

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

Is that consistent with the catch-all range, which overrides the board
facts for every pin?

For reference, Documentation/devicetree/bindings/dpll/dpll-pin.yaml
describes the property as the pin's supported set:

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

Would it work to advertise only the firmware-listed entries (plus the
current frequency) and let sit9531x_output_freq_set() reject the rest with
-EINVAL, rather than widening the advertised set?

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

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

* Re: [PATCH net-next v8 05/15] dpll: sit9531x: register DPLL devices and pins
  2026-09-02 21:40 ` [PATCH net-next v8 05/15] dpll: sit9531x: register DPLL devices and pins Ali Rouhi
@ 2026-09-08 12:45   ` netdev-bot+sashiko
  0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-09-08 12:45 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, 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: 2 · Medium: 6 · Low: 2

- [High] At this commit every pin ops table except the XO one is rejected
  by the DPLL core: `sit9531x_dpll_input_pin_ops` and…
- [High] Blind overwrite in priority table causes tracking mismatch and
  input receiver power leak.
- [Medium] The binding
  Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml (added…
- [Medium] The commit message enumerates the registered pins as "the
  inputs, the crystal, and the outputs the loaded configuration routes…
- [Medium] sit9531x_derive_clock_id() (drivers/dpll/sit9531x/core.c)
  builds the DPLL clock_id from only the SiTime OUI, the variant id and…
- [Medium] sit9531x_irq_thread_fn() (drivers/dpll/sit9531x/core.c) acks a
  fixed set of eight W1C notification registers via…
- [Medium] The driver installs a self-requeuing 2 Hz kthread-worker
  poller (sit9531x_dev_periodic_work(), SIT9531X_STATUS_POLL_MS = 500)…
- [Medium] sit9531x_input_mode_fetch() (drivers/dpll/sit9531x/core.c)
  collapses the two independent CLKINx_INPUT_MODE enable bits into one…
- [Low] drivers/dpll/sit9531x/dpll.c carries a "Pin-state contract"
  comment specifying a get/set state machine over "the five pin ops…
- [Low] sit9531x_dpll_mode_set() (drivers/dpll/sit9531x/dpll.c) clears
  SIT9531X_PLL_STATUS_OUTER_DIS in PLL page reg 0x31 and then issues the…

--

dpll: sit9531x: register DPLL devices and pins

This patch registers the four on-chip PLLs as DPLL devices, registers the
inputs, the crystal and the routed outputs as pins, and adds a 2 Hz
kthread-worker poller (plus an optional INTRB threaded IRQ) that caches
the chip state the lock-status, mode and direction callbacks answer from.

A few questions below.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index f355b08aebca..1394ea1077d9 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c

[ ... ]

> @@ -153,76 +167,1487 @@ int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,

[ ... ]

> +static int sit9531x_input_mode_fetch(struct sit9531x_dev *sitdev, u8 pair)
> +{
> +	enum sit9531x_signal_mode sig_mode;
> +	u8 mode;
> +	int rc;
> +
> +	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_IN_MODE(pair), &mode);
>  	if (rc)
> -		return dev_err_probe(sitdev->dev, rc,
> -				     "Failed to initialize mutex\n");
> +		return rc;
> +
> +	if (mode & (SIT9531X_IN_MODE_SE_P_EN | SIT9531X_IN_MODE_SE_N_EN))
> +		sig_mode = SIT9531X_MODE_SE;
> +	else
> +		sig_mode = SIT9531X_MODE_DE;
> +
> +	sitdev->ref[pair * 2].sig_mode = sig_mode;
> +	sitdev->ref[pair * 2 + 1].sig_mode = sig_mode;

[Severity: Medium]
The register carries two independent bits, SE_P_EN and SE_N_EN, but this
folds them into one pair-wide value and writes it to both lanes.  What
happens on a profile that enables only one of the two single-ended lanes?

sit9531x_input_pin_is_registrable() only rejects the N lane of a
differential pair:

	if (sit9531x_input_is_n(index) &&
	    sitdev->ref[index].sig_mode == SIT9531X_MODE_DE)
		return false;

	return true;

so with a single SE_P_EN set, the N lane is classified SE too and gets its
own DPLL pin.  sit9531x_ref_state_fetch() then derives ref->enabled purely
from the force/state override:

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

and reports that lane as enabled.  Does this publish a pin for a lane whose
receiver the loaded configuration never enabled, which the later prio/state
setters can then add to a PLL's priority table?

[ ... ]

> +	/*
> +	 * 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: Medium]
Can this branch ever run at this commit?  Nothing in the driver reads
"sitime,output-pll-map" or "sitime,pll-fvco" yet - prop.c parses only reg,
label, connection-type, esync-control, supported-frequencies-hz and
dpll-types - and sitdev is zero-allocated, so out_pll_map_valid stays
false.

The binding added earlier in this series already documents both
properties, including:

  sitime,output-pll-map:
    ...
    The value 255 (0xff) marks an output as unmapped and prevents the
    driver from registering it as a DPLL pin.

so a board written against the merged binding is silently ignored here and
falls back to the OUT_MAP bitmaps that the comment above says cannot
unambiguously express the routing.  SIT9531X_OUT_PLL_UNMAPPED and
intsync_src are likewise unused at this point.

The last patch of the series adds sit9531x_parse_board_config() with the
device_property_read_*() calls for both properties, so the gap closes
inside the series; would it be better to keep the binding and these fields
in the same patch as the parser?

[ ... ]

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

[Severity: Medium]
What stops this tick when the parent I2C adapter suspends?

sit9531x_i2c_driver sets only .name, .of_match_table and .probe, so there
are no dev_pm_ops, and kthread workers are not freezable.  The 500 ms tick
re-arms itself unconditionally here, and sitdev->irq is never disabled, so
paged I2C traffic keeps being issued across suspend.  i2c_transfer() fails
those transfers:

drivers/i2c/i2c-core-base.c:i2c_transfer() {
	...
	ret = __i2c_check_suspended(adap);
	if (ret)
		return ret;
	...
}

Can a tick that lands mid-suspend tear a paged sequence between the
page-selector write and the register access, leaving the cached lock
status, LOS/OOF and routing state out of step with the chip?  The driver
still has no .pm member at the end of the series.

> +static irqreturn_t sit9531x_irq_thread_fn(int irq, void *data)
> +{
> +	struct sit9531x_dev *sitdev = data;
> +	int rc;
> +
> +	/*
> +	 * Acknowledge the chip's notification latches from the threaded
> +	 * handler itself.  With IRQF_ONESHOT the line is unmasked on
> +	 * return, so deferring the W1C clear to the async kworker would
> +	 * let a still-asserted INTRB re-fire immediately (interrupt storm).
> +	 * Clear here, then kick the poll worker to read state and run
> +	 * changes_check.
> +	 */
> +	mutex_lock(&sitdev->multiop_lock);
> +	rc = sit9531x_clear_notifications(sitdev);
> +	mutex_unlock(&sitdev->multiop_lock);
> +	if (rc)
> +		dev_warn_ratelimited(sitdev->dev,
> +				     "IRQ: failed to clear notifications: %d\n",
> +				     rc);
> +
> +	kthread_mod_delayed_work(sitdev->kworker, &sitdev->work, 0);
> +	return IRQ_HANDLED;
> +}

[Severity: Medium]
When the acknowledge write itself fails, this still returns IRQ_HANDLED.
With IRQF_ONESHOT and a NULL primary handler the line is unmasked on
return, so a persistently failing I2C ack leaves INTRB asserted and the
threaded handler re-enters back to back, each pass taking multiop_lock and
issuing eight I2C writes plus a worker kick.  genirq's spurious detector
never engages because the return value says handled.

The same shape applies if the asserting latch is outside the acked set:
sit9531x_clear_notifications() covers page 0 regs 0x07/0x0B/0x93/0x9E and
page 6 regs 0x03/0x07/0x93/0x97, but not the sticky EEPROM notify byte at
page 0 reg 0x97 that sit9531x_eeprom_state_report() reads and never
clears.  Is the driver relying on an interrupt-enable/mask state it never
programs before devm_request_threaded_irq()?

[ ... ]

> +	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.  A zero calculated CRC means nothing was read at all.
> +	 */
> +	if (rec_crc != cal_crc || !cal_crc)
> +		dev_warn(sitdev->dev,
> +			 "EEPROM CRC mismatch: stored %08x, computed %08x\n",
> +			 rec_crc, cal_crc);

[Severity: Medium]
The comment a few lines above this function says boards in this family may
have their configuration pushed over I2C rather than held in an EEPROM,
and that "there the CRC pair means nothing".  On such a board both
registers read 0, so the !cal_crc test fires on every probe and logs:

  EEPROM CRC mismatch: stored 00000000, computed 00000000

Should the zero-CRC case be treated as "no EEPROM" instead of a warning?
This probe-time reporting is also not mentioned in the changelog.

[ ... ]

> +static bool sit9531x_dpll_pin_is_registrable(struct sit9531x_dpll *sitdpll,
> +					     enum dpll_pin_direction dir,
> +					     u8 index)
> +{
> +	struct sit9531x_dev *sitdev = sitdpll->dev;
> +
> +	if (dir == DPLL_PIN_DIRECTION_INPUT) {
> +		/* The internal INTSYNC and XO pins are always registrable */
> +		if (index == SIT9531X_INTSYNC_PIN_ID ||
> +		    index == SIT9531X_MAX_INPUTS)
> +			return true;
> +
> +		return sit9531x_input_pin_is_registrable(sitdev, index);
> +	}
> +
> +	/* The internal INTSYNC source pin is always registrable */
> +	if (index == SIT9531X_INTSYNC_OUT_PIN_ID)
> +		return true;

[Severity: Medium]
The changelog describes the registered pins as "the inputs, the crystal,
and the outputs the loaded configuration routes to a PLL" and adds "An
output routed to no PLL gets no pin".

Two more pins are registered here on all four DPLLs: the INTSYNC
destination input (SIT9531X_INTSYNC_PIN_ID) and the INTSYNC source output
(SIT9531X_INTSYNC_OUT_PIN_ID), both before any routing test.  The INTSYNC
source is an output with no PLL routing at all, and prop.c gives both
labels and DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE.  Could the changelog
mention these two pins, since they are visible over netlink?

[ ... ]

> +static u64 sit9531x_derive_clock_id(struct sit9531x_dev *sitdev)
> +{
> +	u64 clkid;
> +
> +	clkid  = SIT9531X_OUI << 24;
> +	clkid |= (u64)sitdev->info->id << 8;
> +	clkid |= (u64)sitdev->client->addr;
> +
> +	return clkid;
> +}

[Severity: Medium]
This identifier is built from the OUI, the variant id and the 7-bit I2C
address only, with nothing identifying the adapter.  Do two same-variant
parts at the same address on different I2C busses (or behind a mux) end up
sharing a clock_id?

The core reuses existing objects on a match:

drivers/dpll/dpll_core.c:dpll_device_get() {
	...
	if (dpll->clock_id == clock_id &&
	    dpll->device_idx == device_idx &&
	    dpll->module == module) {
		__dpll_device_hold(dpll, tracker);
	...
}

and dpll_pin_get() matches on (clock_id, pin_idx, module) in the same way,
so the second chip's four DPLLs and its pins would be folded onto the
first chip's, with the first chip's priv serving netlink requests for
both.  sit9531x_derive_clock_id() is unchanged at the end of the series.

> diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
> index 230b21b9e238..d4159c7ddaf5 100644
> --- a/drivers/dpll/sit9531x/core.h
> +++ b/drivers/dpll/sit9531x/core.h

[ ... ]

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

[ ... ]

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

[Severity: High]
The comment above sit9531x_ref_pll_mask_fetch() in this patch states
the invariant this layer rests on: an input is counted for a PLL
exactly when it appears in that PLL's Page-1 priority table, and that
is "exactly the condition the connect and disconnect callbacks
maintain".  sit9531x_input_prio_add(), declared here and defined in
core.c, does not hold up its end of that.

After failing to find the source already in the table it writes it
into srcs[SIT9531X_PRIO_MAX_SLOTS - 1] unconditionally and commits,
without looking at what that slot held.  When the table already
carries eleven distinct sources, the source sitting in slot 10 is
silently ejected from the hardware table while its
sitdev->ref[].pll_mask bit for this PLL stays set, since only the
DISCONNECTED arm of sit9531x_dpll_input_pin_state_on_dpll_set() ever
clears it.

Two things then drift apart.  The refcount that decides when the
shared receiver may be powered down no longer matches the table, so
the ejected input's receiver can never reach a zero mask and stays
powered for the lifetime of the device even after every DPLL has
released it.  And the state getter, which by the contract comment in
dpll.c derives membership M from the hardware priority table, starts
reporting DISCONNECTED for a pin the driver still believes this DPLL
claims - a reference userspace asked for and was told it got, removed
by an unrelated later request with no error returned to either
caller.

The reachable path is a plain sequence of netlink pin-state writes:
eleven distinct sources set to DPLL_PIN_STATE_SELECTABLE on one DPLL,
then a twelfth.

What should the twelfth request do?  Returning -ENOSPC and leaving
the table alone looks like the honest answer, and it keeps pll_mask
and the table in step by construction.  If displacing an entry is
really wanted, then the displaced source's pll_mask bit has to be
dropped and its receiver released on the same path, and the pin whose
membership just changed needs a dpll_pin_change_ntf().  Which of the
two did you intend?

[Severity: Low]
This isn't a bug, but roughly fifteen functions declared in this hunk have
no definition anywhere in the module at this commit, among them
sit9531x_input_prio_set/get/add/remove, 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.

dpll.h also adds fields nothing reads yet (prio, phase_adjust,
phase_offset, esync_freq), and dpll.c adds SIT9531X_ESYNC_FREQ_10MHZ and
SIT9531X_ESYNC_PULSE_DEFAULT unused.  They all gain definitions and users
in later patches of the series; could the declarations move to the patches
that define them?

> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> new file mode 100644
> index 000000000000..821370ddeab0
> --- /dev/null
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -0,0 +1,417 @@

[ ... ]

> +	/* Trigger small update to apply without full NVM cycle */
> +	rc = sit9531x_write_pll_u8(sitdev, sitdpll->id,
> +				   SIT9531X_PLL_REG_SMALL_UPDATE,
> +				   SIT9531X_SMALL_UPDATE_CMD);
> +	if (rc) {
> +		NL_SET_ERR_MSG(extack, "Failed to trigger small update");
> +		goto unlock;
> +	}

[Severity: Low]
If the small-update write fails here, the OUTER_DIS bit cleared by the
preceding sit9531x_update_pll_u8() is neither committed nor restored.
Reg 0x31 then reads "outer loop enabled" while the loop is still
free-running, and the next poll picks that up:

sit9531x_chan_state_fetch() {
	...
	chan->mode = !!(status & SIT9531X_PLL_STATUS_OUTER_DIS);
	...
}

after which sit9531x_dpll_lock_status_get() takes its chan->mode == 0
branch and reports LOCKED or UNLOCKED from an outer loss-of-lock bit that
means nothing for a disabled outer loop.  Should this path put the bit
back?

[ ... ]

> +static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
> +	.direction_get		= sit9531x_dpll_input_pin_direction_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),
> +};

[Severity: High]
Does probe succeed with this table?  It has no .state_on_dpll_get, and it
sets .supported_ffo without an .ffo_get, both of which the core rejects:

drivers/dpll/dpll_core.c:dpll_pin_register() {
	...
	if (WARN_ON(!ops) ||
	    WARN_ON(!ops->state_on_dpll_get) ||
	    WARN_ON(!ops->direction_get) ||
	    WARN_ON(ops->supported_ffo && !ops->ffo_get) || ...)
		return -EINVAL;
	...
}

sit9531x_dpll_output_pin_ops below has the same gap for
.state_on_dpll_get, so only the XO table passes.  The first registrable
pin is logical input 0, so the chain

  sit9531x_dev_probe() -> sit9531x_devm_dpll_init() ->
  sit9531x_dev_start() -> sit9531x_dpll_register() ->
  sit9531x_dpll_pins_register() -> sit9531x_dpll_pin_register() ->
  dpll_pin_register()

returns -EINVAL with a WARN backtrace, sit9531x_dev_start() takes its
err_unregister path and probe fails on every board (a panic with
panic_on_warn set).

The later patches in this series ("implement input pin state on a DPLL",
"implement output pin state on a DPLL", "model the inter-PLL sync net as a
pair of pins" and "add support to get fractional frequency offset") fill
in .state_on_dpll_get for all five tables and add .ffo_get, so the final
tree is fine; the intermediate commits are what fail to probe.  Could the
callbacks land in the same patch that registers the pins?

[ ... ]

> +		ops = sit9531x_dpll_pin_ops_get(pin);
> +		rc = ops->state_on_dpll_get(pin->dpll_pin, pin,
> +					    sitdpll->dpll_dev, sitdpll,
> +					    &state, NULL);

[Severity: High]
Same missing callback here: state_on_dpll_get is an optional pin op, and
sit9531x_dpll_pin_ops_get() returns sit9531x_dpll_input_pin_ops for every
non-XO input pin, where the member is NULL.  The XO pin is the only one
skipped by the test above it, so would this call dereference NULL if a pin
ever reached this loop?

[ ... ]

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

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

* Re: [PATCH net-next v8 06/15] dpll: sit9531x: implement input pin state on a DPLL
  2026-09-02 21:40 ` [PATCH net-next v8 06/15] dpll: sit9531x: implement input pin state on a DPLL Ali Rouhi
@ 2026-09-08 12:45   ` netdev-bot+sashiko
  0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-09-08 12:45 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, 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: 1 · Medium: 3 · Low: 3 (1)

- [High] At this commit the INTSYNC destination pin (dpin->id ==
  SIT9531X_INTSYNC_PIN_ID == SIT9531X_MAX_INPUTS + 1 == 9) is dispatched…
- [Medium] sit9531x_input_prio_add() unconditionally writes the requested
  source into srcs[SIT9531X_PRIO_MAX_SLOTS - 1] (slot 10) after…
- [Medium] For a differentially configured input pair only the P-lane pin
  is registered (sit9531x_input_pin_is_registrable() returns false for…
- [Medium] sit9531x_prio_table_commit() has no error containment.
- [Low] Three comments added by this patch attribute behaviour to
  sit9531x_input_prio_set() that it does not have.
- [Low] The two-step pin-state transitions in
  sit9531x_dpll_input_pin_state_on_dpll_set() are not unwound when the…
- [Low] The two new exported helpers sit9531x_input_disable() and
  sit9531x_input_enable() perform read-modify-write sequences on…

Pre-existing issues:
- [Low] The pin-state contract comment above the ops tables
  (dpll.c:209-255) says 'The five pin ops tables below' and prescribes…

--

dpll: sit9531x: implement input pin state on a DPLL

This adds state_on_dpll_get/set for input pins, mapping "connected" onto
membership of a PLL's priority table, and adds core helpers to read, add,
remove and reorder table entries plus enable/disable of the physical input
receiver.

A few questions below on the new paths.

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

[Severity: Low]
This isn't a bug introduced by this patch, but the contract comment the
message refers to is unchanged context in this diff, and it describes more
than exists here.  The comment says "The five pin ops tables below", while
at this commit only three exist (input, xo, output).  It also prescribes
DRIVE-role get/set rules, but sit9531x_dpll_output_pin_ops carries only
.direction_get, and it attributes priority ops to the INTSYNC destination
pin while no table has .prio_get/.prio_set yet.

Those pieces do arrive later in the series (the output state patch and the
inter-PLL sync pin-pair patch), so the mismatch is transient.  Could the
commit message avoid crediting this patch with a comment block it does not
add?

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index 1394ea1077d9..f42c41d8a42e 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -184,6 +184,73 @@ 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.
> + */
> +int sit9531x_input_disable(struct sit9531x_dev *sitdev, u8 index)
> +{
> +	struct sit9531x_ref *ref = &sitdev->ref[index];
> +	unsigned int force_reg, state_reg;
> +	u8 pair = sit9531x_input_pair(index);

[Severity: High]
Can this index sitdev->ref[] out of bounds when called for the INTSYNC
destination pin?

At this commit sit9531x_dpll_pin_ops_get() sends every input pin that is not
the XO pin to sit9531x_dpll_input_pin_ops:

	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;

and sit9531x_pin_props_get() advertises DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE
for it, so a netlink DPLL_CMD_PIN_SET reaches the new setter with
dpin->id == SIT9531X_INTSYNC_PIN_ID, which is SIT9531X_MAX_INPUTS + 1.

ref[] is declared as:

	struct sit9531x_ref	ref[SIT9531X_MAX_INPUTS + 1]; /* +1 for xtal */

so ref[SIT9531X_INTSYNC_PIN_ID] is one element past the end and aliases
sitdev->out[0].  sit9531x_input_disable()/enable() have no index check, so
they read sitdev->ref[index].sig_mode through sit9531x_input_get_regs() from
that aliased memory and then write ref->enabled there.

Additionally, sit9531x_input_pair(SIT9531X_INTSYNC_PIN_ID) is 4, so the
force/state write asserts BIT(4) of registers documented in regs.h as:

	/* One bit per input PAIR (bit 0 = CLKIN0, ..., bit 3 = CLKIN3) */

Would an explicit index check in both helpers, and a separate ops table for
the INTSYNC destination, be preferable?  The later patch "dpll: sit9531x:
model the inter-PLL sync net as a pair of pins" adds
sit9531x_dpll_intsync_dst_pin_ops and routes the destination pin there, so
this is gone at the end of the series; the concern is only that this commit
is reachable on its own.

[Severity: Low]
These two helpers are non-static and declared in core.h, they do
read-modify-write on registers shared by all four input pairs, and they write
sitdev->ref[index].enabled which the poll worker also writes in
sit9531x_input_state_fetch().  They neither take nor assert
sitdev->multiop_lock, and the kernel-doc does not state a locking contract.

Every other multi-step helper in the file does assert it, for example
sit9531x_output_disable()/enable(), sit9531x_input_prio_set/add/remove and
sit9531x_intsync_enable()/disable():

	lockdep_assert_held(&sitdev->multiop_lock);

The single caller here does hold the lock, so nothing races today.  Would
adding the assertion plus a "caller must hold sitdev->multiop_lock" line keep
a future caller from clobbering another pair's bits without lockdep noticing?

> +	u8 val;
> +	int rc;
> +
> +	sit9531x_input_get_regs(sitdev, index, &force_reg, &state_reg);
> +
> +	rc = sit9531x_read_u8(sitdev, force_reg, &val);
> +	if (rc)
> +		return rc;
> +	rc = sit9531x_write_u8(sitdev, force_reg, val | BIT(pair));
> +	if (rc)
> +		return rc;
> +
> +	rc = sit9531x_read_u8(sitdev, state_reg, &val);
> +	if (rc)
> +		return rc;

[Severity: Low]
The force override is asserted before its state bit is cleared, and an error
in between returns with no attempt to undo it.  Does that leave the receiver
under a half-programmed override, forced to whatever the stale state bit
says, with ref->enabled not updated and the source already removed from the
PLL's table by the caller?

The output helpers in this file always finish the sequence and keep the first
error:

	ret = sit9531x_prg_commit(sitdev);
	if (ret && !rc)
		rc = ret;

Would the same shape work here?  The matching gap on the SELECTABLE side is
noted further down in dpll.c.

> +	rc = sit9531x_write_u8(sitdev, state_reg, val & ~BIT(pair));
> +	if (rc)
> +		return rc;
> +
> +	ref->enabled = false;
> +
> +	return 0;
> +}
> +

[ ... ]

> @@ -328,6 +432,67 @@ static void sit9531x_prio_mask_build(struct sit9531x_dev *sitdev, u8 pll_idx,
>  	sitdev->chan[pll_idx].prio_mask = mask;
>  }
>  
> +/*
> + * 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.
> + */

[Severity: Low]
This isn't a bug, but the reference is circular.
sit9531x_input_prio_set() contains no HO_CTRL force/clear, no settle delay
and no SIT9531X_SMALL_UPDATE_CMD write; it reads the table, shifts the
entries and calls this function.  The holdover and latch sequence exists only
here.

The same applies to the "Latch unconditionally, as in
sit9531x_input_prio_set()" comment below and to the "(matching
sit9531x_input_prio_set())" note in sit9531x_input_prio_remove(): prio_set is
a pure permutation of existing entries and never backfills tail slots.  All
three comments are still there at the end of the series.

> +static int sit9531x_prio_table_commit(struct sit9531x_dev *sitdev, u8 pll_idx,
> +				      const u8 *srcs)
> +{
> +	u8 val, slot;
> +	int rc, prg_rc, ho_rc;
> +	u16 reg;
> +
> +	rc = sit9531x_update_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_HO_CTRL,
> +				    BIT(SIT9531X_PLL_HO_FORCE_BIT),
> +				    BIT(SIT9531X_PLL_HO_FORCE_BIT));
> +	if (rc)
> +		return rc;
> +
> +	usleep_range(10000, 12000);
> +
> +	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
> +		reg = sit9531x_prio_reg(pll_idx, slot);
> +
> +		rc = sit9531x_read_u8(sitdev, reg, &val);
> +		if (rc)
> +			goto commit;
> +
> +		val = sit9531x_prio_slot_set(val, slot, srcs[slot]);
> +
> +		rc = sit9531x_write_u8(sitdev, reg, val);
> +		if (rc)
> +			goto commit;
> +	}
> +
> +commit:
> +	/* Latch unconditionally, as in sit9531x_input_prio_set(). */
> +	prg_rc = sit9531x_prio_prg_commit(sitdev);
> +	if (prg_rc && !rc)
> +		rc = prg_rc;
> +
> +	/*
> +	 * 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: Medium]
Is the unconditional mask refresh correct on the error paths?

If the very first sit9531x_read_u8() in the loop fails, no register was
modified at all, yet the code still falls into the commit label, issues the
small-change latch, and then records the fully intended srcs[] in
chan->prio_mask.  The same happens when sit9531x_prio_prg_commit() itself
fails.

chan->prio_mask is exactly what the new
sit9531x_dpll_selection_state_get() consults:

	if (!(chan->prio_mask & BIT(sit9531x_input_hw_src(pin_id))))
		*state = DPLL_PIN_STATE_DISCONNECTED;

so userspace is told about membership the hardware never adopted, until the
next successful poll, or indefinitely if the bus keeps failing.

Separately, a mid-loop failure latches a half-rewritten table (new compacted
prefix, stale tail that may still hold the source being removed) and then
releases forced holdover, so the PLL re-selects from a table nobody intended,
with no restore of the original.  Would refreshing the mask only after a
fully successful, latched sequence be safer?

> +
> +	ho_rc = sit9531x_update_pll_u8(sitdev, pll_idx,
> +				       SIT9531X_PLL_REG_HO_CTRL,
> +				       BIT(SIT9531X_PLL_HO_FORCE_BIT), 0);
> +	if (ho_rc && !rc)
> +		rc = ho_rc;
> +
> +	return rc;
> +}
> +

[ ... ]

> @@ -352,6 +517,171 @@ static int sit9531x_prio_table_read(struct sit9531x_dev *sitdev, u8 pll_idx,
>  	return 0;
>  }
>  

[ ... ]

> +/*
> + * sit9531x_input_prio_add - make an input eligible in a PLL's table
> + * @input_idx:	input source in hardware encoding
> + *
> + * Ensures the source appears in the priority table so it can be picked
> + * by automatic reference selection again after a disconnect.  If the
> + * source is already listed the table is left untouched; otherwise it is
> + * placed in the lowest-priority slot.  The original priority is not
> + * restored -- use sit9531x_input_prio_set() to reassign it.
> + *
> + * Caller must hold sitdev->multiop_lock.
> + */
> +int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
> +			    u8 input_idx)
> +{
> +	u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
> +	u8 slot;
> +	int rc;
> +
> +	lockdep_assert_held(&sitdev->multiop_lock);
> +
> +	if (pll_idx >= SIT9531X_NUM_PLLS)
> +		return -EINVAL;
> +
> +	rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
> +	if (rc)
> +		return rc;
> +
> +	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++)
> +		if (srcs[slot] == input_idx)
> +			return 0;
> +
> +	srcs[SIT9531X_PRIO_MAX_SLOTS - 1] = input_idx;

[Severity: Medium]
Does this silently evict whatever already occupied the last slot?

The 4-bit slot encoding documented in regs.h has no "empty" value:

 *   0=IN0P, 1=IN1P, 2=IN2P, 3=IN3P, 4=IN4P,
 *   5=OCXO, 6=INTSYNC,
 *   7=IN0N, 8=IN1N, 9=IN2N, 10=IN3N, 11=IN4N

so if the lowest-priority slot holds a distinct configured last-resort source
(the OCXO, code 5, for instance) it is overwritten here and 0 is returned as
success.  That input then disappears from this PLL's selection table, and its
state_on_dpll_get() flips to DISCONNECTED although userspace never asked.

Because sit9531x_dpll_input_pin_state_on_dpll_set() only updates the
ref->pll_mask of the pin the request named, and pll_mask is otherwise seeded
only once at probe by sit9531x_ref_pll_mask_fetch(), the evicted input keeps
this PLL's bit set forever, so the shared receiver is never released.

sit9531x_input_prio_remove() below establishes a "duplicated tail slot"
convention for freed slots.  Could prio_add() look for such a duplicated
slot, or return -ENOSPC when the table is genuinely full, instead of
clobbering slot SIT9531X_PRIO_MAX_SLOTS - 1?  This is unchanged at the end of
the series.

> +
> +	return sit9531x_prio_table_commit(sitdev, pll_idx, srcs);
> +}
> +

[ ... ]

> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index 821370ddeab0..29088707a3e6 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -255,6 +255,41 @@ const struct dpll_device_ops sit9531x_dpll_device_ops = {

[ ... ]

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

[Severity: Medium]
For a differentially configured pair, only the P-lane pin is registered
(sit9531x_input_pin_is_registrable() returns false for the N lane of a DE
pair), so sit9531x_input_hw_src(pin_id) here always yields the P code.
Should the N-lane code be folded onto the P lane, the way the probe path
already does?

drivers/dpll/sit9531x/core.c:sit9531x_ref_pll_mask_fetch() {
	...
			if (sit9531x_input_is_n(index) &&
			    sitdev->ref[index].sig_mode == SIT9531X_MODE_DE)
				index--;
	...
}

sit9531x_prio_mask_build() records the raw source code with no folding:

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

so if a PLL's table lists such a pair under SIT9531X_PRIO_SRC_N_BASE + pair,
the test above misses it and the pin reads back DISCONNECTED.
chan->selected_ref is derived as pair * 2 + 1 by sit9531x_hw_src_input(), so
CONNECTED can never be reported for that pin either.

The setter has the mirror problem: sit9531x_input_prio_remove() with the P
code does not find the N-coded entry, takes the "!found -> return 0" path,
and the caller then clears ref->pll_mask and calls sit9531x_input_disable().
Does that gate off the receiver while the source is still an eligible table
entry for the PLL, and report success to userspace?  A SELECTABLE set
conversely adds the P code next to the existing N code, listing the same
physical pair twice.  This is also unchanged at the end of the series.

> +	else if (active_input)
> +		*state = DPLL_PIN_STATE_CONNECTED;
> +	else
> +		*state = DPLL_PIN_STATE_SELECTABLE;
> +}
> +

[ ... ]

> @@ -267,8 +302,125 @@ 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)
> +{
> +	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);
                   ^^^^
This is the entry point for the ref[] indexing question raised above on
sit9531x_input_disable(): dpin->id can be SIT9531X_INTSYNC_PIN_ID here.

> +	u8 pll_bit = BIT(sitdpll->id);
> +	int rc;
> +
> +	mutex_lock(&sitdev->multiop_lock);
> +
> +	switch (state) {
> +	case DPLL_PIN_STATE_DISCONNECTED:
> +		rc = sit9531x_input_prio_remove(sitdev, sitdpll->id, hw_src);
> +		if (rc)
> +			break;
> +		ref->pll_mask &= ~pll_bit;
> +		if (ref->pll_mask)
> +			rc = 0;	/* another DPLL still uses this input */
> +		else
> +			rc = sit9531x_input_disable(sitdev, dpin->id);
> +		break;

[ ... ]

> +	case DPLL_PIN_STATE_SELECTABLE:
> +		rc = sit9531x_input_enable(sitdev, dpin->id);
> +		if (rc)
> +			break;
> +		rc = sit9531x_input_prio_add(sitdev, sitdpll->id, hw_src);
> +		if (rc)
> +			break;

[Severity: Low]
When sit9531x_input_prio_add() fails here, the receiver has already been
enabled and ref->pll_mask is left at zero.  Is that an enable with no
matching claim, so nothing will ever pair it with a
sit9531x_input_disable()?  Would calling sit9531x_input_disable() on this
error path keep the enable and the claim in step?

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

[ ... ]

> +	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 705176c35ddd..dd027142b370 100644
> --- a/drivers/dpll/sit9531x/regs.h
> +++ b/drivers/dpll/sit9531x/regs.h
> @@ -132,6 +132,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)

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

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

* Re: [PATCH net-next v8 07/15] dpll: sit9531x: add support to get and set priority on input pins
  2026-09-02 21:40 ` [PATCH net-next v8 07/15] dpll: sit9531x: add support to get and set priority on input pins Ali Rouhi
@ 2026-09-08 12:45   ` netdev-bot+sashiko
  0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-09-08 12:45 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, 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] sit9531x_input_prio_get()
  (drivers/dpll/sit9531x/core.c:456-457) returns success with *prio =…
- [Medium] Setting one input pin's priority silently renumbers other
  pins' priorities without notifying them.
- [Medium] A demotion request through the newly wired .prio_set can be a
  complete no-op at the register level while the callback reports…
- [Medium] sit9531x_input_prio_get() decides presence purely by comparing
  each of the 11 nibbles against the input's hardware source code ('if…
- [Low] The kernel-doc added by this patch for sit9531x_input_prio_get()
  states "Scans the PLL's 12-slot priority table on Page 1"…
- [Low] With .prio_set/.prio_get exposed, a userspace-configured priority
  is volatile and the field meant to preserve it is dead code.
- [Low] sit9531x_input_prio_get() issues one I2C read per slot (11 reads)
  although the slots are nibble-packed two per register…
- [Low] The 'dpin->dir != DPLL_PIN_DIRECTION_INPUT' guard in
  sit9531x_dpll_input_pin_prio_set() (dpll.c:467-470) is unreachable…

--

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.  The
getter scans the per-PLL priority table over I2C and reports the slot the
input occupies, and the setter moves the input to the requested slot via
sit9531x_input_prio_set().

A few questions below about the value reported for inputs absent from the
table, about the interaction with the tail-backfill done by
sit9531x_input_prio_remove(), and about a couple of comments.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index f42c41d8a42e..e706f6942f65 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -414,6 +414,49 @@ static int sit9531x_prio_prg_commit(struct sit9531x_dev *sitdev)
>  	return 0;
>  }
>  
> +/*
> + * sit9531x_input_prio_get - read an input's priority slot for a PLL
> + * @input_idx:	input source in hardware encoding (see
> + *		sit9531x_input_hw_src())
> + * @prio:	output slot position (0 = highest); set to
> + *		SIT9531X_PRIO_MAX_SLOTS when the source is not in the table
> + *
> + * Scans the PLL's 12-slot priority table on Page 1 and returns the
                        ^^^^^^^

[Severity: Low]
This isn't a bug, but should this say 11-slot?  regs.h has:

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

and the block comment already in core.c above sit9531x_prio_reg() says "The
SiT9531x has an 11-slot priority table per PLL on Page 1."  The loop in the
function itself stops at SIT9531X_PRIO_MAX_SLOTS, so 12 looks like the source
encoding count rather than the slot count, and a later reader could conclude
that slot 11 exists and is programmable when that nibble is
CLK_ACTIVESEL_PLL.

> + * highest-priority (lowest-numbered) slot that references the source.
> + * This reads the value the chip actually holds rather than a cached
> + * default, so pin-get reflects the real hardware priority.
> + *
> + * Caller must hold sitdev->multiop_lock.
> + */
> +int sit9531x_input_prio_get(struct sit9531x_dev *sitdev, u8 pll_idx,
> +			    u8 input_idx, u8 *prio)
> +{
> +	u8 val, slot, src;
> +	int rc;
> +
> +	lockdep_assert_held(&sitdev->multiop_lock);
> +
> +	if (pll_idx >= SIT9531X_NUM_PLLS)
> +		return -EINVAL;
> +
> +	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
> +		rc = sit9531x_read_u8(sitdev,
> +				      sit9531x_prio_reg(pll_idx, slot), &val);
> +		if (rc)
> +			return rc;

[Severity: Low]
Could this loop read each register only once?  The slots are nibble-packed two
per register:

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

so slots 0 and 1 map to the same register, 2 and 3 to the next, and so on,
giving 11 reads where 6 would do.  The regmap is created with .cache_type =
REGCACHE_NONE, so each of those is a real bus transfer plus page-selector
traffic, all under multiop_lock, which the 500 ms monitor kthread also takes.

sit9531x_prio_table_read() already exists and returns the whole table in one
pass; would using it here (or reusing the table the poll worker already reads
before sit9531x_prio_mask_build() discards the slot numbers) be simpler?

> +
> +		src = sit9531x_prio_slot_get(val, slot);
> +
> +		if (src == input_idx) {
> +			*prio = slot;
> +			return 0;
> +		}

[Severity: Medium]
Can a nibble that was never programmed be mistaken for IN0P here?  regs.h
documents the encoding as:

   *   0=IN0P, 1=IN1P, 2=IN2P, 3=IN3P, 4=IN4P,
   *   5=OCXO, 6=INTSYNC,
   *   7=IN0N, 8=IN1N, 9=IN2N, 10=IN3N, 11=IN4N

There is no reserved "slot unused" value, and sit9531x_input_hw_src(0)
returns 0 for IN0P, so a slot left at 0 by the efuse/blob configuration
compares equal to IN0P.  In that case IN0 is reported as occupying the first
such slot (often slot 0, the highest priority) on every PLL even if it was
never added, the "not in the table" path below can never be taken for that
input, and the -EINVAL guard in sit9531x_input_prio_set() would let the table
be rearranged around an entry that is not really there.

I could not find code that initialises the priority block to a known state at
probe, so whether unprogrammed slots really read 0 depends on the device
configuration.  Is that guaranteed not to happen, or should a match be
qualified against sources the driver knows it programmed?

> +	}
> +
> +	*prio = SIT9531X_PRIO_MAX_SLOTS;
> +	return 0;
> +}

[Severity: Medium]
Is priority 11 a value userspace can be handed?  This returns success with
*prio = SIT9531X_PRIO_MAX_SLOTS, and sit9531x_dpll_input_pin_prio_get()
forwards it unchanged:

	dpin->prio = slot;
	*prio = slot;
	return 0;

The core has no "no value" convention on that path; dpll_msg_add_pin_prio()
calls ops->prio_get and, on success, does:

	if (nla_put_u32(msg, DPLL_A_PIN_PRIO, prio))
		return -EMSGSIZE;

So every input not present in the PLL table is reported as DPLL_A_PIN_PRIO
11, while the setter added in the same patch rejects exactly that value:

	if (prio >= SIT9531X_PRIO_MAX_SLOTS) {
		NL_SET_ERR_MSG(extack, "Priority out of range (0-10)");
		return -EINVAL;
	}

A dump/modify/restore round trip of a disconnected input then fails with
EINVAL.  Since this is a new driver the value becomes uAPI once released.

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 the code match that?  As written the core is handed 11, which is
out of range for this driver.

> +
>  /*
>   * 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
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index 29088707a3e6..67b9fbba9f2e 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -417,10 +417,90 @@ 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
> + *
> + * reads the PLL's priority table on Page 1 (via
> + * sit9531x_input_prio_get()) and returns the slot the input
> + * occupies, so pin-get reports the real hardware priority rather
> + * than a software default.
> + */
> +static int
> +sit9531x_dpll_input_pin_prio_get(const struct dpll_pin *pin, void *pin_priv,
> +				 const struct dpll_device *dpll, void *dpll_priv,
> +				 u32 *prio, struct netlink_ext_ack *extack)
> +{
> +	struct sit9531x_dpll_pin *dpin = pin_priv;
> +	struct sit9531x_dpll *sitdpll = dpll_priv;
> +	struct sit9531x_dev *sitdev = sitdpll->dev;
> +	u8 slot;
> +	int rc;
> +
> +	mutex_lock(&sitdev->multiop_lock);
> +	rc = sit9531x_input_prio_get(sitdev, sitdpll->id,
> +				     sit9531x_input_hw_src(dpin->id), &slot);
> +	mutex_unlock(&sitdev->multiop_lock);
> +	if (rc)
> +		return rc;
> +
> +	dpin->prio = slot;

[Severity: Low]
Is anything reading dpin->prio?  It is written here and again at the end of
sit9531x_dpll_input_pin_prio_set(), and I could not find a read of it
anywhere in the driver, at this commit or at the end of the series.  Writing
driver state from a getter is also a side effect in an otherwise read-only
op, and the value stored can be the out-of-range 11 discussed above.

Related to that: a configured priority does not survive a disconnect and
reconnect.  DPLL_PIN_STATE_DISCONNECTED goes through
sit9531x_input_prio_remove(), which compacts the table, and
DPLL_PIN_STATE_SELECTABLE goes through sit9531x_input_prio_add(), which
always lands the source in the last slot:

	srcs[SIT9531X_PRIO_MAX_SLOTS - 1] = input_idx;

with its kernel-doc stating "The original priority is not restored".  So a
pin configured with prio 0 comes back as prio 10.  Meanwhile the natural
order of operations (set priority, then connect) is refused, since
sit9531x_input_prio_set() returns -EINVAL for a source absent from the table
and the callback answers "Pin is not a reference of this DPLL; connect it
first".  Would using dpin->prio to re-apply the priority on reconnect close
that, or should the field be dropped?

> +	*prio = slot;
> +	return 0;
> +}
> +
> +/*
> + * sit9531x_dpll_input_pin_prio_set - set input pin priority
> + *
> + * writes input priority table on Page 1 via
> + * core.c sit9531x_input_prio_set().  Forces holdover during update.
> + */
> +static int
> +sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
> +				 const struct dpll_device *dpll, void *dpll_priv,
> +				 u32 prio, struct netlink_ext_ack *extack)
> +{
> +	struct sit9531x_dpll_pin *dpin = pin_priv;
> +	struct sit9531x_dpll *sitdpll = dpll_priv;
> +	struct sit9531x_dev *sitdev = sitdpll->dev;
> +	int rc;
> +
> +	if (dpin->dir != DPLL_PIN_DIRECTION_INPUT) {
> +		NL_SET_ERR_MSG(extack, "Priority applies only to input pins");
> +		return -EINVAL;
> +	}

[Severity: Low]
Can this branch ever be taken?  sit9531x_dpll_pin_ops_get() only installs
sit9531x_dpll_input_pin_ops for pins that are inputs:

	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;

and sit9531x_dpll_is_input_pin() is just pin->dir == DPLL_PIN_DIRECTION_INPUT.
There is no .direction_set in any of the driver's ops tables, so dir cannot
change after registration.  The getter added in the same patch has no such
check, which makes the pair inconsistent too.

> +
> +	if (prio >= SIT9531X_PRIO_MAX_SLOTS) {
> +		NL_SET_ERR_MSG(extack, "Priority out of range (0-10)");
> +		return -EINVAL;
> +	}
> +
> +	mutex_lock(&sitdev->multiop_lock);
> +	rc = sit9531x_input_prio_set(sitdev, sitdpll->id,
> +				     sit9531x_input_hw_src(dpin->id),
> +				     (u8)prio);
> +	mutex_unlock(&sitdev->multiop_lock);

[Severity: Medium]
Can a demotion request through this path end up a no-op that still reports
success?

sit9531x_input_prio_remove() deliberately duplicates the last kept source
into the freed tail slots, since the 4-bit encoding has no empty value:

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

So a table like [A,B,C,C,C,C,C,C,C,C,C] is a normal state.  If userspace now
asks to move C to slot 6, sit9531x_input_prio_set() picks the first match:

	for (from = 0; from < SIT9531X_PRIO_MAX_SLOTS; from++)
		if (srcs[from] == input_idx)
			break;
	...
		for (slot = from; slot < prio; slot++)
			srcs[slot] = srcs[slot + 1];

	srcs[prio] = input_idx;

from is 2, and the shift copies C over C, so the committed array is identical
to what the chip already holds.  sit9531x_prio_table_commit() still forces the
PLL into holdover:

	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));
	...
	usleep_range(10000, 12000);

rewrites the same nibbles, releases holdover and returns 0.  The effective
priority stays 2 because the device picks the lowest occupied slot, so a set
followed by a get disagree, and the PLL took a holdover excursion for a write
that changed nothing.

[Severity: Medium]
Should the other affected pins get a change notification here?

sit9531x_input_prio_set() shifts every entry between the old and the new slot:

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

The slot number is what .prio_get reports as DPLL_A_PIN_PRIO, so several other
pins change priority as a side effect of one request.

The core only notifies the pin named in the request, in dpll_pin_prio_set():

	__dpll_pin_change_ntf(pin);

and the driver's periodic sit9531x_dpll_changes_check() only compares the
result of state_on_dpll_get() against pin->pin_state, never priority.  Does a
userspace cache of the other pins' priorities then stay stale indefinitely
with no DPLL_CMD_PIN_CHANGE_NTF?

> +
> +	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;
> +	}
> +
> +	dpin->prio = (u8)prio;
> +
> +	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,

[ ... ]

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

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

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

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

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

- [Medium] sit9531x_get_fvco() returns 0 both for a register-read failure
  (DIVN registers on the PLL page, or the XO-doubler bit at page 0 reg…
- [Medium] A per-pin frequency set has PLL-wide and NVM side effects that
  the commit message does not disclose: sit9531x_prg_commit() writes…
- [Medium] sit9531x_dpll_output_pin_frequency_get() converts every
  failure of sit9531x_output_freq_get() into success with the stale…
- [Medium] The new input .frequency_get reports sitdev->ref[].freq, which
  prop.c seeds with the *first* entry of the DT 'supported-frequencies-…
- [Medium] prop.c appends an unconditional catch-all supported-frequency
  range of 1 Hz..1 GHz to every ordinary pin, so the freq_supported set…
- [Medium] The new output frequency setter is reachable with values it
  never advertised, because the DPLL core narrows the u64 netlink…
- [Medium] sit9531x_output_phase_flush() sets
  SIT9531X_PLL_PHFL_ON_DEMAND_EN (PLL page reg 0x3D bit 7) and latches…
- [Medium] An I2C failure partway through the five-byte DIVO write leaves
  a mixed old/new 34-bit divider staged, and the common `goto commit`…
- [Medium] Truncation of computed output frequency to `u32` without
  bounds checking.
- [Low] DISCARDED (fixed later in the series).
- [Low] The kernel-doc block of the newly added sit9531x_get_fvco()
  explains it as a fix for 'the previous split between free-run and sync…
- [Low] sit9531x_output_freq_get()'s kernel-doc documents only '0 on
  success, -ENODEV when the output divider or VCO rate is not…

--

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

This adds .frequency_get for input, XO and output pins and .frequency_set
for output pins.  Input frequencies are reported from the cached firmware
description, while output frequencies are computed from the DIVO divider
read back from the chip and programmed by writing a new divider inside the
chip's programming state.

A few questions about the new code below.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index e706f6942f65..721b2c451d59 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -339,6 +339,35 @@ static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev, u8 slot,
>  	return 0;
>  }
>  
> +/*
> + * 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;
> +
> +	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
> +			       SIT9531X_UPDATE_NVM);

[Severity: Medium]
The commit message describes an output frequency set only as "computed from
the divider read back from the chip and set by writing a new one".  Does a
single pin set also carry two side effects the message does not mention?

sit9531x_dpll_output_pin_frequency_set()
  sit9531x_output_freq_set()
    sit9531x_prg_commit()            /* SIT9531X_UPDATE_NVM + msleep(100) */
    sit9531x_output_phase_flush(pll_idx)

The NVM shadow update and the unconditional 100 ms sleep run on every
successful set, and the flush comment states that it makes "every output
divider of @pll_idx restart aligned to the PLL phase".

Does that mean setting the frequency of one output phase-steps every other
output driven by the same PLL, discarding whatever per-output phase adjust
was previously programmed on those siblings?  Could the changelog say that
a per-pin set is PLL-wide and touches the NVM shadow?

> +
> +	/*
> +	 * 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.
> +	 */
> +	rc2 = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
> +				SIT9531X_LOOP_LOCK);
> +
> +	msleep(100);
> +
> +	return rc ? rc : rc2;
> +}
> +
>  /*
>   * Input priority selection
>   *

[ ... ]

> @@ -735,6 +769,414 @@ int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
>  #define SIT9531X_FVCO_HIGHBAND_MIN		6875000000ULL
>  #define SIT9531X_FVCO_HIGHBAND_MAX		7812500000ULL
>  

[ ... ]

> +/*
> + * sit9531x_get_fvco - read VCO frequency from chip's DIVN registers
> + *
> + * Fvco = Fref * DIVN, where DIVN comes from sit9531x_divn_static() and
> + * Fref = xtal_freq << doubler.  DIVN is the
> + * steady-state Fvco/Fref target programmed by the NVM blob and is
> + * authoritative in both free-run and sync modes; the previous split
> + * between free-run and sync formulas returned 0 on chips that didn't
> + * have a sync input populated, which broke the TDC phase readback.
> + *
> + * Return: Fvco in Hz, or 0 on error
> + */

[Severity: Low]
This comment describes "the previous split between free-run and sync
formulas returned 0 on chips that didn't have a sync input populated, which
broke the TDC phase readback".  Which upstream code is that referring to?

This patch adds the first Fvco computation in the driver, so there is no
previous split to have been replaced.  There is also no TDC phase readback
at this point in the series: sit9531x_phase_offset_read() is only declared
in core.h, with no definition in any .c file yet.

Could the paragraph be dropped, or reworded so it does not read as a fix
for a regression that never existed in tree?

> +static u64 sit9531x_get_fvco(struct sit9531x_dev *sitdev, u8 pll_idx)
> +{
> +	int doubler, rc;
> +	s64 divn;
> +	u64 fref;
> +
> +	/*
> +	 * DT board-config override: some configs (e.g. an INTSYNC PLL)
> +	 * run a VCO that Fref*DIVN does not reproduce.  When the board
> +	 * supplies the measured VCO, use it verbatim.
> +	 */
> +	if (pll_idx < SIT9531X_NUM_PLLS && sitdev->pll_fvco[pll_idx])
> +		return sitdev->pll_fvco[pll_idx];
> +
> +	rc = sit9531x_divn_static(sitdev, pll_idx, &divn);
> +	if (rc || divn <= 0)
> +		return 0;
> +
> +	doubler = sit9531x_is_xo_doubler_enabled(sitdev);
> +	if (doubler < 0)
> +		return 0;
> +
> +	fref = (u64)sitdev->xtal_freq << doubler;
> +
> +	return mul_u64_u64_div_u64(fref, (u64)divn, SIT9531X_DIVN_SCALE);
> +}
> +
> +/*
> + * sit9531x_output_phase_flush - flush the output phase of a PLL

[ ... ]

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

[Severity: Medium]
Is SIT9531X_PLL_PHFL_ON_DEMAND_EN meant to stay set after the flush?

sit9531x_output_phase_flush() sets it on the PLL page and latches it with
the small-change update, but no path in the driver clears it again -- a grep
for PHFL in drivers/dpll/sit9531x/ finds only this write site.  Only the
Page 0 trigger-source select is restored to orig at the end.

That leaves the PLL permanently armed, so a later assertion of the restored
(possibly GPIO-sourced) DIVO phase trigger re-flushes every output divider
of that PLL, which is the persistent output phase offset the flush was added
to avoid.

The function's own comment argues the trigger source must not stay pinned
after a one-shot flush; should the other half of the same one-shot sequence
be unwound too, or is the enable intentionally sticky and just missing a
comment saying so?

> +
> +	/* Latch it with the PLL small-change update. */
> +	rc = sit9531x_update_pll_u8(sitdev, pll_idx,
> +				    SIT9531X_PLL_REG_SMALL_UPDATE,
> +				    SIT9531X_SMALL_UPDATE_CMD,
> +				    SIT9531X_SMALL_UPDATE_CMD);
> +	if (rc)
> +		return rc;

[ ... ]

> +	ret = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, orig);
> +	if (ret && !rc)
> +		rc = ret;
> +
> +	return rc;
> +}
> +
> +/*
> + * sit9531x_output_freq_set - set output clock frequency

[ ... ]

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

[ ... ]

> +	fvco = sit9531x_get_fvco(sitdev, pll_idx);
> +	if (!fvco) {
> +		fvco = fvco_min;
> +	} else if (!sitdev->pll_fvco[pll_idx]) {
> +		if (fvco < fvco_min)
> +			fvco = fvco_min;
> +		else if (fvco > fvco_max)
> +			fvco = fvco_max;
> +	}

[Severity: Medium]
Is it safe to program a real divider from a guessed VCO rate here?

sit9531x_get_fvco() returns 0 for two very different situations:

	rc = sit9531x_divn_static(sitdev, pll_idx, &divn);
	if (rc || divn <= 0)
		return 0;

	doubler = sit9531x_is_xo_doubler_enabled(sitdev);
	if (doubler < 0)
		return 0;

so an unprogrammed DIVN and a failed regmap read of the DIVN registers or
of the doubler bit are indistinguishable.

In either case this code substitutes the band-edge constant fvco_min, then
DIVO is computed from it, written to the chip, committed to the NVM shadow
and the loops re-locked, and 0 is returned.  For a low-band PLL whose real
VCO is near 5.89824 GHz but whose readback failed, does that not put the
output roughly 20% away from what user space asked for while reporting
success?

The clamp path has the same question: an out-of-band readback is silently
pulled to the band edge rather than reported.

Would returning an error when the VCO cannot be read be preferable, and
should sit9531x_get_fvco() distinguish a bus error from a dormant PLL?

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

[Severity: Medium]
Can a request that the driver advertised as supported be satisfied at a
materially different rate here?

sit9531x_pin_props_get() in prop.c appends a catch-all range to every
ordinary pin:

	/* Always append a wide catch-all range */
	ranges[j].min = 1;
	ranges[j].max = 1000000000ULL; /* 1 GHz */

dpll_pin_freq_set() accepts anything inside prop.freq_supported, and this
code only rejects a zero DIVO.  With a low-band Fvco of 4915200000 Hz, a
request for 1 GHz gives DIVO = 4, so the pin emits 1228800000 Hz -- neither
the requested value nor inside the advertised range -- and the call returns
success.  Smaller residues appear everywhere else, e.g. 156250000 Hz becomes
158554838 Hz on the same VCO.

Should either the advertised ranges describe rates the divider can actually
produce, or the setter reject requests it cannot hit exactly?

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

[ ... ]

> +	divo_bytes[0] = (divo >>  0) & 0xFF;
> +	divo_bytes[1] = (divo >>  8) & 0xFF;
> +	divo_bytes[2] = (divo >> 16) & 0xFF;
> +	divo_bytes[3] = (divo >> 24) & 0xFF;
> +	divo_bytes[4] = (divo >> 32) & 0x03;  /* only bits [1:0] */
> +
> +	rc = sit9531x_read_u8(sitdev,
> +			      SIT9531X_REG(page, base_reg - 4), &msb_old);
> +	if (rc)
> +		goto commit;
> +	divo_bytes[4] |= msb_old & 0xFC;
> +
> +	for (j = 0; j < 5; j++) {
> +		rc = sit9531x_write_u8(sitdev,
> +				       SIT9531X_REG(page, base_reg - j),
> +				       divo_bytes[j]);
> +		if (rc)
> +			goto commit;
> +	}
> +
> +commit:

[Severity: Medium]
What happens to the 34-bit divider when one of these five writes fails
partway through?

The bytes are written one register at a time, so a failure at j > 0 leaves a
mixed old/new DIVO staged, and the shared exit then applies it:

commit:
	ret = sit9531x_prg_commit(sitdev);   /* UPDATE_NVM then LOOP_LOCK */

Does that leave the output running at a frequency nobody requested, with the
NVM shadow updated to match, while the function returns an error and
sitdev->out[out_idx].freq still describes the old rate?

The commit message covers the exit running on failure ("the exit runs even
when a write in the middle failed and the first error is the one returned"),
but not the half-written field.  Would saving and restoring the five
pre-write bytes on the failure path, or abandoning the staged update, be
possible here?

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

[ ... ]

> +	sitdev->out[out_idx].freq = (u32)div64_u64(fvco, divo);
> +
> +	return 0;
> +}
> +
> +/*
> + * sit9531x_output_freq_get - read output clock frequency from hardware
> + * @out_idx:	output index (0-N for this chip variant)
> + * @frequency:	output frequency in Hz

[Severity: Medium]
Is a u32 wide enough for the effective rate this line caches?

struct sit9531x_out.freq is a u32, but the value stored here is
fvco / divo, and divo is only rejected when it is zero.  With divo == 1
the effective rate is the raw VCO, 4.9-5.9 GHz on a low-band PLL and
6.9-7.8 GHz on a high-band one, all above U32_MAX (~4.29 GHz), so the
cast silently wraps: a 4915200000 Hz output would be cached as
620232704 Hz.  sit9531x_output_freq_get() does the same thing one
function below with (u32)*frequency, so a read-back of such an output
overwrites the cache with the same wrapped value even though
*frequency itself is correct.

That matters because sit9531x_dpll_output_pin_frequency_get() falls
back to sit9531x_out_state_get(sitdev, dpin->id)->freq whenever the
register read path returns an error, so user space can be handed the
wrapped rate as a live frequency, and dpll_pin_freq_set() compares its
request against that same value.

Would widening sit9531x_out.freq to u64 be the intended fix, or should
sit9531x_output_freq_set() reject requests whose divider would come out
below the smallest DIVO the hardware supports, so a rate the field
cannot represent is never programmed in the first place?

[ ... ]

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

[Severity: Low]
The Return: block lists 0, -ENODEV and register access errors, but this
-EINVAL for an out-of-range out_idx is not among them.  Could it be added?

In the same area, the doc block of sit9531x_output_freq_set() numbers a
seven-step write sequence while the in-body comments label the same points
"Step 4" and "Step 5", so the two numberings do not line up.

> +
> +	pll_idx = sitdev->out[out_idx].pll_idx;
> +	if (pll_idx >= SIT9531X_NUM_PLLS)
> +		return -ENODEV;
> +
> +	fvco = sit9531x_get_fvco(sitdev, pll_idx);
> +	if (!fvco)
> +		return -ENODEV;

[ ... ]

> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index 67b9fbba9f2e..a7510ca721b6 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -302,6 +302,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;
> +}

[Severity: Medium]
The changelog says "An input's frequency is what the board presents".  Is
ref->freq actually that value when a board lists several rates?

sit9531x_pin_props_get() in prop.c seeds it with the first list entry:

	if (num_freqs > 0) {
		if (dir == DPLL_PIN_DIRECTION_INPUT)
			sitdev->ref[index].freq = (u32)freqs[0];

The generic binding in Documentation/devicetree/bindings/dpll/dpll-pin.yaml
describes supported-frequencies-hz only as a "List of supported frequencies
for this pin, expressed in Hz", so list order carries no current-rate
meaning.

With no input .frequency_set and no hardware measurement, does this report
an arbitrary entry of the supported list as the pin's live frequency, with
no way for user space to correct it?

[Severity: Low]
Separately, does this read past ref[] for the INTSYNC pin at this point in
the series?

sit9531x_dpll_pin_ops_get() returns sit9531x_dpll_input_pin_ops for the
INTSYNC destination pin, whose dpin->id is SIT9531X_INTSYNC_PIN_ID (9),
while sitdev->ref[] is dimensioned SIT9531X_MAX_INPUTS + 1 (9 entries), so
ref[9] aliases out[0].  The error fallback in the output getter below has
the same shape with out[12] aliasing chan[0].

The later series patch "dpll: sit9531x: model the inter-PLL sync net as a
pair of pins" adds dedicated INTSYNC ops tables with no frequency callbacks
and a comment saying exactly this, so the path is gone by the end of the
series -- would it be worth ordering that patch before this one so no commit
in between exposes ref[9] and out[12] as pin frequencies?

>  /*
>   * sit9531x_dpll_input_pin_state_on_dpll_get - get input pin DPLL state
>   *

[ ... ]

> @@ -576,8 +600,79 @@ 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 when the
> + * output is not resolvable through the divider chain (e.g. not mapped
> + * to a PLL), so a netlink dump never fails on such pins.
> + */
> +static int
> +sit9531x_dpll_output_pin_frequency_get(const struct dpll_pin *pin,
> +				       void *pin_priv,
> +				       const struct dpll_device *dpll,
> +				       void *dpll_priv, u64 *frequency,
> +				       struct netlink_ext_ack *extack)
> +{
> +	struct sit9531x_dpll_pin *dpin = pin_priv;
> +	struct sit9531x_dpll *sitdpll = dpll_priv;
> +	struct sit9531x_dev *sitdev = sitdpll->dev;
> +	int rc;
> +
> +	mutex_lock(&sitdev->multiop_lock);
> +	rc = sit9531x_output_freq_get(sitdev, dpin->id, frequency);
> +	mutex_unlock(&sitdev->multiop_lock);
> +
> +	if (rc)
> +		*frequency = sit9531x_out_state_get(sitdev, dpin->id)->freq;
> +
> +	return 0;
> +}

[Severity: Medium]
The comment justifies the fallback for outputs "not resolvable through the
divider chain (e.g. not mapped to a PLL)", but does this also swallow I2C
and regmap errors?

sit9531x_output_freq_get() propagates the error of sit9531x_read_u8() for
each of the five DIVO byte reads, and sit9531x_get_fvco() reads the DIVN
registers and the doubler bit as well:

		rc = sit9531x_read_u8(sitdev,
				      SIT9531X_REG(page, base_reg - j), &v);
		if (rc)
			return rc;

With every non-zero rc turned into "return 0" plus the stale cached value,
a transient bus failure is reported to user space as a valid live frequency.

Does it also affect the set path?  dpll_pin_freq_set() calls
ops->frequency_get() first and then:

	if (freq == old_freq)
		return 0;

so if the stale cache happens to equal the requested frequency, the set is
reported as successful without frequency_set() ever running.

Would distinguishing -ENODEV (the unresolvable case the comment describes)
from register access errors work here?

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

[ ... ]

> +	mutex_lock(&sitdev->multiop_lock);
> +	rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll,
> +				      frequency);
> +	mutex_unlock(&sitdev->multiop_lock);

[Severity: Medium]
Should this setter sanity-check the upper bound of frequency itself?

The core validates the request narrowed to 32 bits before handing over the
full 64-bit value:

drivers/dpll/dpll_netlink.c:dpll_pin_freq_set() {
	u64 freq = nla_get_u64(a), old_freq;
	...
	if (!dpll_pin_is_freq_supported(pin, freq)) {
	...
	ret = ops->frequency_set(pin, ..., freq, extack);
}

with

static bool dpll_pin_is_freq_supported(struct dpll_pin *pin, u32 freq)

Combined with the 1 Hz..1 GHz catch-all range this driver advertises, does a
request of 4294967297 Hz validate as 1 Hz and then arrive here at full
width?  sit9531x_output_freq_set() only rejects a zero DIVO, so for a
documented band VCO it would compute DIVO = 1 and program the output at the
raw VCO rate of 4.9-7.8 GHz while returning success, and the u32 cache
update would wrap.

The narrowing helper predates this series, but this is the first driver
callback that can act on it.

[ ... ]

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

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

* Re: [PATCH net-next v8 09/15] dpll: sit9531x: implement output pin state on a DPLL
  2026-09-02 21:40 ` [PATCH net-next v8 09/15] dpll: sit9531x: implement output " Ali Rouhi
@ 2026-09-08 12:45   ` netdev-bot+sashiko
  0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-09-08 12:45 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, 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: 0 · Medium: 2 · Low: 1

- [Medium] The new `.state_on_dpll_get`/`.state_on_dpll_set` callbacks
  are installed into `sit9531x_dpll_output_pin_ops`, which…
- [Medium] `sit9531x_output_disable()` and `sit9531x_output_enable()`
  program the Page-0x03 Hi-Z force/state banks with a sequence of read-…
- [Low] The banner comment on `sit9531x_output_disable()`
  (drivers/dpll/sit9531x/core.c:409) states it "Sets MASK+STATE on BOTH…

--

dpll: sit9531x: implement output pin state on a DPLL

This adds .state_on_dpll_get / .state_on_dpll_set for output pins, mapping
CONNECTED and DISCONNECTED onto the Page 0x03 Hi-Z force/state register
pairs.  New core helpers sit9531x_output_enable() and
sit9531x_output_disable() wrap the register writes in the PRG_CMD / NVM
update / loop lock sequence and update the cached out[].enabled flag.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index 721b2c451d59..0f886cfd1401 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c

[ ... ]

> @@ -368,6 +402,115 @@ static int sit9531x_prg_commit(struct sit9531x_dev *sitdev)
>  	return rc ? rc : rc2;
>  }
>
> +/*
> + * sit9531x_output_disable - mute an output (force Hi-Z)
> + * @index:	logical output index (0..info->num_outputs-1)
> + *
> + * Sets MASK+STATE on BOTH the DIFF and SE register pairs so that the
> + * output is muted regardless of its electrical configuration.  The

[Severity: Low]
This isn't a bug, but does the banner describe the wrong polarity?  It says
MASK+STATE are set to mute, while the body sets MASK and clears STATE:

	/* Take control (MASK=1) and mute (STATE=0) on both DIFF and SE */
	rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, true);
	...
	rc = sit9531x_hiz_set_bit(sitdev, r.diff_state, r.bit, false);

The register description above sit9531x_output_get_hiz_regs() agrees with the
body:

 * MASK bit = 1  -> driver takes control of that output's Hi-Z state
 * STATE bit = 0 -> output is forced to Hi-Z (muted)

and so does the reader sit9531x_output_forced_hiz():

	*muted = (mask & BIT(r.bit)) && !(state & BIT(r.bit));

Could the banner be reworded to say "sets MASK and clears STATE"?

> + * writes are wrapped in the PRG_CMD / NVM update / loop lock sequence
> + * so the new state is applied by the hardware.
> + *
> + * Caller must hold sitdev->multiop_lock.
> + */
> +int sit9531x_output_disable(struct sit9531x_dev *sitdev, u8 index)
> +{
> +	const struct sit9531x_chip_info *info = sitdev->info;
> +	struct sit9531x_hiz_regs r;
> +	u8 slot;
> +	int rc, ret;
> +
> +	lockdep_assert_held(&sitdev->multiop_lock);
> +
> +	if (index >= info->num_outputs)
> +		return -EINVAL;
> +
> +	slot = info->clkout_map[index];
> +	sit9531x_output_get_hiz_regs(slot, &r);
> +
> +	rc = sit9531x_prg_enter(sitdev);
> +	if (rc)
> +		return rc;
> +
> +	/* Take control (MASK=1) and mute (STATE=0) on both DIFF and SE */
> +	rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, true);
> +	if (rc)
> +		goto commit;
> +	rc = sit9531x_hiz_set_bit(sitdev, r.diff_state, r.bit, false);
> +	if (rc)
> +		goto commit;
> +	rc = sit9531x_hiz_set_bit(sitdev, r.se_mask, r.bit, true);
> +	if (rc)
> +		goto commit;
> +	rc = sit9531x_hiz_set_bit(sitdev, r.se_state, r.bit, false);
> +
> +commit:
> +	/*
> +	 * Always leave the PRG_CMD programming state, even on a mid-sequence
> +	 * write failure: prg_enter() unlocked the output loops, so returning
> +	 * without prg_commit() would strand the chip in the programming state
> +	 * with the loops unlocked.  Best effort -- keep the first error.
> +	 */
> +	ret = sit9531x_prg_commit(sitdev);
> +	if (ret && !rc)
> +		rc = ret;
> +	if (!rc)
> +		sitdev->out[index].enabled = false;
> +
> +	return rc;
> +}

[Severity: Medium]
Can a partial register sequence here leave sitdev->out[index].enabled
permanently disagreeing with the hardware?

Suppose the diff_mask write succeeds and the diff_state write returns an I2C
error:

	rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, true);	/* ok */
	if (rc)
		goto commit;
	rc = sit9531x_hiz_set_bit(sitdev, r.diff_state, r.bit, false);	/* -EIO */
	if (rc)
		goto commit;

MASK=1 with STATE still 0 from the loaded configuration is the muted encoding
per the register notes ("STATE bit = 0 -> output is forced to Hi-Z"), and
sit9531x_prg_commit() then issues UPDATE_NVM + LOOP_LOCK so the partial
programming is applied.  But rc is nonzero, so:

	if (!rc)
		sitdev->out[index].enabled = false;

is skipped and enabled stays true.

The mirror case in sit9531x_output_enable() is the same: if the diff_mask
clear succeeds and the se_mask clear fails, a DIFF-configured pad is already
released and driving while enabled stays false.

Is there anything that later reconciles the cache?  The hardware readback
sit9531x_output_forced_hiz() is reached only via sit9531x_out_state_fetch(),
whose only caller is sit9531x_dev_state_fetch() on the probe/start path:

	for (i = 0; i < sitdev->info->num_outputs; i++) {
		rc = sit9531x_out_state_fetch(sitdev, i);

The periodic worker refreshes only ref and chan state, and the new getter
reads only the cached flag, so the wrong state would be reported over netlink
for the life of the bound device.

Would it be better to record the intended state (or mark it unknown and
re-read the force/state registers) even when rc is nonzero?

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

[ ... ]

> +	rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, false);
> +	if (rc)
> +		goto commit;
> +	rc = sit9531x_hiz_set_bit(sitdev, r.se_mask, r.bit, false);
> +
> +commit:

[ ... ]

> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index a7510ca721b6..6365a83e4c52 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -669,10 +669,79 @@ 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;
> +	const struct sit9531x_out *out;
> +
> +	out = sit9531x_out_state_get(sitdpll->dev, dpin->id);
> +	*state = out->enabled ? DPLL_PIN_STATE_CONNECTED
> +			      : DPLL_PIN_STATE_DISCONNECTED;
> +	return 0;
> +}

[Severity: Medium]
Does this read past sitdev->out[] for the internal INTSYNC source pin?

sit9531x_dpll_pin_ops_get() hands this ops table to every non-input pin, with
no id filtering:

	if (!sit9531x_dpll_is_input_pin(pin))
		return &sit9531x_dpll_output_pin_ops;

and sit9531x_dpll_pin_is_registrable() always registers a pin whose id is
SIT9531X_INTSYNC_OUT_PIN_ID:

	/* The internal INTSYNC source pin is always registrable */
	if (index == SIT9531X_INTSYNC_OUT_PIN_ID)
		return true;

core.h defines that id as the array size:

	#define SIT9531X_INTSYNC_OUT_PIN_ID	SIT9531X_MAX_OUTPUTS
	...
	struct sit9531x_out	out[SIT9531X_MAX_OUTPUTS];

so sit9531x_out_state_get(sitdev, 12) resolves to &sitdev->out[12], one
element past the end, and ->enabled is read from whatever follows in struct
sit9531x_dev (chan[0]).  That value is then reported to user space as the pin
state on every DPLL_CMD_PIN_GET dump.

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

The same pin reaches the setter with dpin->id == 12, which both helpers reject
because 12 >= info->num_outputs:

	if (index >= info->num_outputs)
		return -EINVAL;

yet sit9531x_pin_props_get() advertises that pin as changeable:

	} else if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
		   index == SIT9531X_INTSYNC_OUT_PIN_ID) {
		...
		props->dpll_props.capabilities =
			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;

so user space is told the state can change but always gets -EINVAL.  Would it
make sense to route the INTSYNC source pin to its own ops table, or at least
bound-check dpin->id against info->num_outputs in the getter?

For what it is worth, a later patch in this series, "dpll: sit9531x: model the
inter-PLL sync net as a pair of pins", adds sit9531x_dpll_intsync_src_pin_ops
and dispatches the INTSYNC source pin to it in sit9531x_dpll_pin_ops_get(),
which removes both problems.  Could that dispatch be folded in here, or this
patch ordered after it, so no intermediate commit exposes the out-of-range
access?

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

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

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

* Re: [PATCH net-next v8 11/15] dpll: sit9531x: add support to get and set esync on pins
  2026-09-02 21:40 ` [PATCH net-next v8 11/15] dpll: sit9531x: add support to get and set esync on pins Ali Rouhi
@ 2026-09-08 12:45   ` netdev-bot+sashiko
  0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-09-08 12:45 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, 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: 3 · Low: 2

- [High] Embedded-sync frequency is implemented as the output's carrier
  frequency.
- [High] sit9531x_dpll_output_pin_esync_set() is asymmetric and destroys
  the only record of the pin's previous rate.
- [Medium] sit9531x_dpll_output_pin_esync_get() reports esync state from
  an unsynchronized software cache: it returns dpin->esync_freq and a…
- [Medium] The uAPI duty-cycle percentage constant is written verbatim
  into a hardware register field.
- [Medium] The esync enable path chains three independently committed
  hardware transactions with no rollback: sit9531x_output_freq_set()…
- [Low] sit9531x_dpll_output_pin_esync_get() reports esync->pulse = 50
  unconditionally, including when it reports esync->freq == 0 (embedded…
- [Low] The new PROG0 write is wrapped in its own
  prg_enter()/prg_commit() window even though PROG0 lives in the same…

--

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

This adds esync_get and esync_set callbacks to the sit9531x output pin ops
and a new core helper that programs the per-output ODRn_PROG0 pulse control
byte.  Only outputs flagged as esync-control accept the operations, the
rest return -EOPNOTSUPP.

No Link: tags are present in the commit header.

A few questions below.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index be033d7cfe7c..c93578f3b750 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -1535,6 +1535,57 @@ 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;
> +	u8 slot, page, reg;
> +	int rc, ret;
> +
> +	lockdep_assert_held(&sitdev->multiop_lock);
> +
> +	if (out_idx >= info->num_outputs)
> +		return -EINVAL;
> +
> +	slot = info->clkout_map[out_idx];
> +	page = (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX) ?
> +		SIT9531X_PAGE_OUTSYS1 : SIT9531X_PAGE_OUTSYS0;
> +	reg = SIT9531X_OUT_PROG0_BASE +
> +	      SIT9531X_OUT_PRG_SLOT_STRIDE * (slot % 6);
> +
> +	/*
> +	 * PROG0 lives in the output system, so like the DIVO and
> +	 * PRG_RST_DELAY writes it only takes effect inside the PRG_CMD
> +	 * programming state committed to the NVM shadow.
> +	 */
> +	rc = sit9531x_prg_enter(sitdev);
> +	if (rc)
> +		return rc;
> +
> +	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, reg), pulse_ctrl);

[Severity: Medium]
Is any conversion needed between the value passed in as pulse_ctrl and the
hardware field?  The only caller passes SIT9531X_ESYNC_PULSE_DEFAULT, which
is decimal 50, and the same constant is handed to userspace as esync->pulse
in sit9531x_dpll_output_pin_esync_get().  The uAPI unit for that attribute
is percent:

Documentation/netlink/specs/dpll.yaml
        name: esync-pulse
        type: u32
        doc: |
          A ratio of high to low state of a SYNC signal pulse embedded
          into base clock frequency. Value is in percents.

The new regs.h comment in this patch describes the same register as a pulse
count byte for SYSREF / SYNCB modes and defines no field masks, so it looks
like one constant is being used both as a uAPI percentage and as a raw
register value.  Which of the two is it?

Separately, this is an unmasked full-byte write with no read-modify-write.
The neighbouring PROG6 programming in sit9531x_output_phase_adjust_set()
does preserve the upper bits:

	/* PROG6 RMW: preserve OPSTG_VCASC_BUMP in [7:5] */
	rc = sit9531x_read_u8(sitdev, SIT9531X_REG(page, base), &prog6_val);
	...
	prog6_val &= SIT9531X_OUT_PRG_OPSTG_MASK;

Can writing all eight bits of ODRn_PROG0 clear other fields in that byte
that the initial config blob programmed?

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

[Severity: Low]
This isn't a bug, but the comment above states PROG0 shares the PRG_CMD
programming state with the DIVO and PRG_RST_DELAY writes, so could this
byte be written inside the window sit9531x_output_freq_set() already opens?

As written, one call to sit9531x_dpll_output_pin_esync_set() runs three
separate windows:

  sit9531x_output_freq_set()        -> prg_enter()/prg_commit()
  sit9531x_output_pulse_ctrl_set()  -> prg_enter()/prg_commit()
  sit9531x_output_enable()          -> prg_enter()/prg_commit()

and sit9531x_prg_commit() issues UPDATE_NVM, then LOOP_LOCK, then an
unconditional msleep(100), so that is roughly 300 ms of multiop_lock hold
time plus three unlock/relock cycles of the output loops on a live output.

>  /*
>   * 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 c07fb2256510..76d7d7d21d3d 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -25,6 +25,17 @@
>  #define SIT9531X_ESYNC_FREQ_10MHZ	10000000ULL
>  #define SIT9531X_ESYNC_PULSE_DEFAULT	50
>  
> +static const struct dpll_pin_frequency sit9531x_esync_ranges[] = {
> +	DPLL_PIN_FREQUENCY(0),
> +	DPLL_PIN_FREQUENCY(SIT9531X_ESYNC_FREQ_10MHZ),
> +};

[Severity: High]
Is the embedded sync frequency here the marker rate or the carrier rate?

The dpll uAPI describes DPLL_A_PIN_ESYNC_FREQUENCY as a signal embedded
into the pin's separately configured base clock:

Documentation/netlink/specs/dpll.yaml
        name: esync-frequency
        type: u64
        doc: |
          Frequency of Embedded SYNC signal. If provided, the pin is configured
          with a SYNC signal embedded into its base clock frequency.

This range advertises 0 and 10 MHz as the acceptable esync frequencies, and
sit9531x_dpll_output_pin_esync_set() then feeds the accepted 10 MHz into
sit9531x_output_freq_set(), which programs the DIVO output divider, i.e.
the base clock of the pad.  A 1 PPS esync request is rejected with -EINVAL.

For comparison, ice_dpll_output_esync_get() treats 10 MHz as the required
base frequency and reports the marker rate separately:

	if (!(p->flags[0] & ICE_AQC_GET_CGU_OUT_CFG_ESYNC_ABILITY) ||
	    p->freq != DPLL_PIN_FREQUENCY_10_MHZ)
		return -EOPNOTSUPP;
	...
		esync->freq = DPLL_PIN_FREQUENCY_1_HZ;

The driver's own binding example also pairs esync-control with a base rate
that esync_set would overwrite:

Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
    pin@0 {
        reg = <0>;
        label = "clkout0";
        esync-control;
        supported-frequencies-hz = /bits/ 64 <156250000>;
    };

Enabling esync on that pin changes the pad from 156.25 MHz to 10 MHz.  Is
that intended?

The changelog says:

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

That does not seem to match the code: the only accepted non-zero value is
10 MHz, not 1 Hz, and the freq == 0 path only calls
sit9531x_output_disable() rather than restoring a rate.  Could the
changelog be adjusted to describe what the code does?

> +
> +static inline bool
> +sit9531x_dpll_esync_pin_supported(const struct sit9531x_dpll_pin *dpin)
> +{
> +	return dpin->esync_control;
> +}
> +
>  static inline bool sit9531x_dpll_is_input_pin(const struct sit9531x_dpll_pin *pin)
>  {
>  	return pin->dir == DPLL_PIN_DIRECTION_INPUT;
> @@ -785,6 +796,92 @@ 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;
> +
> +	if (!sit9531x_dpll_esync_pin_supported(dpin))
> +		return -EOPNOTSUPP;
> +
> +	esync->range = sit9531x_esync_ranges;
> +	esync->range_num = ARRAY_SIZE(sit9531x_esync_ranges);
> +	esync->pulse = SIT9531X_ESYNC_PULSE_DEFAULT;
> +	esync->freq = dpin->esync_freq;

[Severity: Medium]
Can dpin->esync_freq go stale here?  Unlike
sit9531x_dpll_output_pin_frequency_get(), this getter does no DIVO or
PROG0 readback, and dpin->esync_freq is written only by esync_set().

Two writers of the same divider leave it inconsistent:

  sit9531x_dpll_output_pin_frequency_set()
     -> sit9531x_output_freq_set(sitdev, dpin->id, actual_pll, frequency)

  sit9531x_dpll_output_pin_state_on_dpll_set(DISCONNECTED)
     -> sit9531x_output_disable(sitdev, dpin->id)

Neither clears dpin->esync_freq, so after pin_set(esync-frequency=10000000)
followed by pin_set(frequency=25000000) a pin dump still advertises an
embedded sync configuration the device is no longer producing.  Note that
dpll_pin_set_from_nlattr() handles DPLL_A_PIN_FREQUENCY and
DPLL_A_PIN_ESYNC_FREQUENCY independently in message order, so both can
arrive in one request.

Userspace also cannot repair the stale value, because dpll_pin_esync_set()
consults the getter first:

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

so a repeat request for the cached value is acked without the driver
callback running.

Also, sit9531x_dpll_pin_register() copies only the capability flag:

	pin->esync_control = props->esync_control;

so an output that the efuse/NVM blob already programmed for esync reads
back as disabled.  Should the getter read the hardware instead?

[Severity: Low]
Should esync->pulse be reported as 0 when esync->freq is 0?  As written a
pin dump on a never-configured esync-control output reports
DPLL_A_PIN_ESYNC_FREQUENCY = 0 together with DPLL_A_PIN_ESYNC_PULSE = 50,
i.e. a 50 percent pulse ratio for a signal that is not present.  Both
ice_dpll_output_esync_get() and zl3073x_dpll_output_pin_esync_get() zero
both fields in the disabled case:

	} else {
		esync->freq = 0;
		esync->pulse = 0;
	}

> +
> +	return 0;
> +}
> +
> +static int
> +sit9531x_dpll_output_pin_esync_set(const struct dpll_pin *pin,
> +				   void *pin_priv,
> +				   const struct dpll_device *dpll,
> +				   void *dpll_priv,
> +				   u64 freq,
> +				   struct netlink_ext_ack *extack)
> +{
> +	struct sit9531x_dpll_pin *dpin = pin_priv;
> +	struct sit9531x_dpll *sitdpll = dpll_priv;
> +	struct sit9531x_dev *sitdev = sitdpll->dev;
> +	u8 actual_pll;
> +	int rc;
> +
> +	if (!sit9531x_dpll_esync_pin_supported(dpin)) {
> +		NL_SET_ERR_MSG(extack,
> +			       "Embedded sync not enabled for this pin");
> +		return -EOPNOTSUPP;
> +	}
> +
> +	actual_pll = sitdev->out[dpin->id].pll_idx;
> +
> +	mutex_lock(&sitdev->multiop_lock);
> +
> +	/*
> +	 * This output is a dedicated embedded-sync pin.
> +	 * Treat freq=0 as a request to disable the entire output.
> +	 */
> +	if (!freq) {
> +		rc = sit9531x_output_disable(sitdev, dpin->id);
> +		if (!rc)
> +			dpin->esync_freq = 0;
> +		mutex_unlock(&sitdev->multiop_lock);
> +		return rc;
> +	}

[Severity: High]
Where is the pin's previous output rate saved so this path can restore it?

The enable path below calls sit9531x_output_freq_set(), which reprograms
DIVO and also replaces the cached rate:

drivers/dpll/sit9531x/core.c:sit9531x_output_freq_set() {
	...
	sitdev->out[out_idx].freq = (u32)div64_u64(fvco, divo);
}

and sit9531x_output_disable() touches only the Hi-Z bits:

	/* Take control (MASK=1) and mute (STATE=0) on both DIFF and SE */

Neither struct sit9531x_dpll_pin nor struct sit9531x_out holds the
pre-esync frequency or the previous PROG0 byte, and
sit9531x_output_pulse_ctrl_set() never reads PROG0 back, so after an
enable/disable cycle the rate userspace originally configured appears to be
gone from both hardware and driver state.

Does a following pin_set(state=connected) then un-mute the pad at 10 MHz
with the esync pulse still armed?

drivers/dpll/sit9531x/dpll.c:sit9531x_dpll_output_pin_state_on_dpll_set() {
	case DPLL_PIN_STATE_CONNECTED:
		rc = sit9531x_output_enable(sitdev, dpin->id);
	...
}

while sit9531x_dpll_output_pin_esync_get() reports esync->freq == 0.

> +
> +	if (freq != SIT9531X_ESYNC_FREQ_10MHZ) {
> +		mutex_unlock(&sitdev->multiop_lock);
> +		NL_SET_ERR_MSG(extack,
> +			       "Only 10 MHz esync frequency is supported");
> +		return -EINVAL;
> +	}
> +
> +	rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll,
> +				      SIT9531X_ESYNC_FREQ_10MHZ);
> +	/*
> +	 * Program the pulse generator (PROG0 PULSE_CTRL) so the embedded-sync
> +	 * pulse is actually emitted; without it the output carries the clock
> +	 * but no esync marker.  SIT9531X_ESYNC_PULSE_DEFAULT is the same duty
> +	 * the esync_get callback advertises.
> +	 */
> +	if (!rc)
> +		rc = sit9531x_output_pulse_ctrl_set(sitdev, dpin->id,
> +						    SIT9531X_ESYNC_PULSE_DEFAULT);
> +	if (!rc)
> +		rc = sit9531x_output_enable(sitdev, dpin->id);
> +
> +	mutex_unlock(&sitdev->multiop_lock);
> +
> +	if (!rc)
> +		dpin->esync_freq = SIT9531X_ESYNC_FREQ_10MHZ;
> +
> +	return rc;
> +}

[Severity: Medium]
What happens if the second or third step here fails?

Each of the three calls commits independently: sit9531x_output_freq_set()
writes DIVO and runs prg_commit() with UPDATE_NVM plus LOOP_LOCK, and
sit9531x_output_pulse_ctrl_set() opens its own PRG_CMD window, so this is
not one transaction.  If sit9531x_output_pulse_ctrl_set() or
sit9531x_output_enable() returns an I2C or regmap error, the divider change
stays committed and nothing is unwound.

Since dpin->esync_freq is only assigned when all three steps return 0, the
driver then reports esync as disabled on a pin whose carrier has already
been changed to 10 MHz.  Is that state recoverable through this attribute?
dpll_pin_esync_set() short-circuits on the cached value:

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

so a follow-up request for freq == 0 matches the stale cached 0 and never
reaches the driver.

There is also a window between the frequency commit and the PROG0 commit
where an already-enabled output emits the new rate with no marker.

>  static const struct dpll_pin_ops sit9531x_dpll_output_pin_ops = {

[ ... ]

> diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
> index 91d33abd15c3..251c98dc7cbe 100644
> --- a/drivers/dpll/sit9531x/regs.h
> +++ b/drivers/dpll/sit9531x/regs.h
> @@ -215,6 +215,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

The comment says this byte applies in SYSREF / SYNCB modes.  The new esync
path does not select either mode, so does the write take effect at all in
the mode the output is left in?

[ ... ]

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

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

* Re: [PATCH net-next v8 10/15] dpll: sit9531x: add support to adjust output phase
  2026-09-02 21:40 ` [PATCH net-next v8 10/15] dpll: sit9531x: add support to adjust output phase Ali Rouhi
@ 2026-09-08 12:45   ` netdev-bot+sashiko
  0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-09-08 12:45 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, 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: 7 · Low: 1

- [Medium] sit9531x_dpll_output_pin_phase_adjust_get()
  (drivers/dpll/sit9531x/dpll.c:753) returns the software cache…
- [Medium] A negative phase request is encoded as an absolute delay
  'T_out - (|phase| mod T_out)' using the output period in force at the…
- [Medium] sit9531x_output_phase_adjust_set() (core.c:1364-1366) rejects
  every request with -EINVAL when the cached output frequency…
- [Medium] The commit message presents the operation as shifting one
  output in time 'against the others driven by the same PLL', but…
- [Medium] Only the negative branch of sit9531x_output_phase_adjust_set()
  reduces the request modulo the output period; the positive branch…
- [Medium] The driver now installs .phase_adjust_get/.phase_adjust_set
  for output pins (dpll.c:789-796) but never populates struct…
- [Medium] The 34-bit PRG_RST_DELAY word is written as five independent
  I2C byte writes, most-significant first (core.c:1438-1478), and every…
- [Low] The newly added register-map comment in regs.h ('Outputs 0-5 are
  on Page 3, outputs 6-11 on Page 4.

--

dpll: sit9531x: add support to adjust output phase

This adds .phase_adjust_get/.phase_adjust_set for sit9531x output pins.  The
requested picosecond offset is split into a 34-bit coarse delay in VCO cycles
and a 3-bit fine field of 30 ps steps, written into the per-output
PROG6..PROG2 block from inside the PRG_CMD programming state.  The write is
followed by an on-demand phase flush on the owning PLL.

A few questions below about the register programming and about how the
value is reported back to userspace.

> dpll: sit9531x: add support to adjust output phase
>
> 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.

[Severity: Medium]
Is the described scope of the operation complete?  The tail of
sit9531x_output_phase_adjust_set() calls:

	return sit9531x_output_phase_flush(sitdev, pll_idx);

and that helper's own comment says:

	 * Fires the chip's on-demand phase-flush (PHFL) so every output divider
	 * of @pll_idx restarts aligned to the PLL phase.

So adjusting one output also restarts the divider phase of every sibling
output on the same PLL, which looks like a phase step on clocks the user did
not touch.

The path also goes through sit9531x_prg_enter() (loops unlocked) and
sit9531x_prg_commit(), which writes SIT9531X_UPDATE_NVM, then
SIT9531X_LOOP_LOCK, then msleep(100).  Could the commit message mention the
NVM shadow write, the sibling-output phase restart, and the ~100 ms cost per
call?

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index 0f886cfd1401..be033d7cfe7c 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -1342,6 +1342,149 @@ 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)
> +		return -EINVAL;

[Severity: Medium]
Should this bail out apply to every request?  freq is only consumed in the
negative branch below, yet a zero cache rejects phase_adjust = 0 and all
positive values too.

sitdev->out[].freq is only written in three places: sit9531x_prop_pin_get(),
sit9531x_output_freq_set() and sit9531x_output_freq_get().  In
sit9531x_prop_pin_get() it is seeded only when the pin fwnode was found and
lists frequencies:

	if (num_freqs <= 0) {
		num_freqs = 0;
		goto skip_fwnode_props;
	}
	...
		else
			sitdev->out[index].freq = (u32)freqs[0];

supported-frequencies-hz is optional in
Documentation/devicetree/bindings/dpll/dpll-pin.yaml ("required: - reg"), and
nothing seeds out[].freq at probe.  On a board whose DT omits it, does every
phase-adjust request return -EINVAL forever?  Would reading the frequency
back from the device, or restricting the check to the negative branch, be
better here?

> +
> +	fvco = sit9531x_get_fvco(sitdev, pll_idx);
> +	if (!fvco)
> +		return -EIO;
> +
> +	/*
> +	 * Convert to unsigned absolute delay.  Negative phase (advance)
> +	 * is rendered as T_out - |phase|, modulo the output period.
> +	 */
> +	if (phase_ps == 0) {
> +		abs_ps = 0;
> +	} else if (phase_ps > 0) {
> +		abs_ps = (u64)phase_ps;
> +	} else {
> +		u64 t_out_ps = div64_u64(1000000000000ULL, freq);
> +		u64 advance = (u64)(-(s64)phase_ps);
> +
> +		if (t_out_ps == 0)
> +			return -EINVAL;
> +		/*
> +		 * 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);
> +		abs_ps = (advance == 0) ? 0 : (t_out_ps - advance);
> +	}

[Severity: Medium]
Should the positive branch also reduce modulo the output period?  The commit
message states:

    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.

but only the negative branch performs that reduction; the positive branch
does abs_ps = (u64)phase_ps verbatim.

sit9531x_prop_pin_get() advertises the full window for outputs:

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

so +1000000 ps on a 10 MHz output (phase-equivalent to 0) programs millions
of VCO cycles of reset delay instead of at most one 100 ns period.  Since
PRG_RST_DELAY is a real reset delay applied when the divider restarts, does
that hold the output off for the whole interval rather than shifting its
phase?

[Severity: Medium]
A related question about the period-relative encoding: the value written here
depends on the output period in force at the time of the call, but nothing
recomputes it when the frequency changes later.

sit9531x_dpll_output_pin_frequency_set() -> sit9531x_output_freq_set() only
rewrites the DIVO bytes and then re-applies the phase reset:

	rc = sit9531x_output_phase_flush(sitdev, pll_idx);
	if (rc)
		return rc;

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

A grep for phase_adjust in drivers/dpll/sit9531x/ shows no path that rewrites
PROG6..PROG2 or invalidates dpin->phase_adjust on a frequency change.  So
-100 ps at 10 MHz (encoded as ~99900 ps) becomes roughly +16567 ps after a
switch to 12 MHz, while the getter still reports -100 ps.  Should the delay be
reprogrammed from the cached request when the output frequency changes?

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

[ ... ]

> +	/* PROG6 RMW: preserve OPSTG_VCASC_BUMP in [7:5] */
> +	rc = sit9531x_read_u8(sitdev, SIT9531X_REG(page, base),
> +			      &prog6_val);
> +	if (rc)
> +		goto commit;
> +
> +	prog6_val &= SIT9531X_OUT_PRG_OPSTG_MASK;
> +	prog6_val |= (fine << SIT9531X_OUT_PRG_FINE_SHIFT) &
> +		     SIT9531X_OUT_PRG_FINE_MASK;
> +	prog6_val |= (u8)((coarse >> 32) & SIT9531X_OUT_PRG_COARSE_HI_MASK);
> +
> +	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base),
> +			       prog6_val);
> +	if (rc)
> +		goto commit;
> +	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base + 1),
> +			       (u8)((coarse >> 24) & 0xFF));
> +	if (rc)
> +		goto commit;
> +	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base + 2),
> +			       (u8)((coarse >> 16) & 0xFF));
> +	if (rc)
> +		goto commit;
> +	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base + 3),
> +			       (u8)((coarse >> 8) & 0xFF));
> +	if (rc)
> +		goto commit;
> +	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base + 4),
> +			       (u8)(coarse & 0xFF));
> +
> +commit:
> +	/*
> +	 * Always leave the PRG_CMD state via prg_commit(), even on a
> +	 * mid-sequence write failure, so the output loops are re-locked rather
> +	 * than stranded unlocked; keep the first error.
> +	 */
> +	ret = sit9531x_prg_commit(sitdev);

[Severity: Medium]
Can a mid-sequence I2C failure commit a half-written delay word?  The 34-bit
field is written as five independent byte writes, most significant first, and
every failure jumps to the same label, where sit9531x_prg_commit()
unconditionally latches whatever is in the registers:

drivers/dpll/sit9531x/core.c:sit9531x_prg_commit() {
	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
			       SIT9531X_UPDATE_NVM);
	...
	rc2 = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
				SIT9531X_LOOP_LOCK);
}

With an old value of 0x00_0000_00FF and a new value of 0x00_FF00_0000, a
failure after the PROG5 write leaves 0xFF0000FF, about 4.28e9 VCO cycles,
which is far larger than either the old or the new intent and well past the
~7.8e6 cycles a 1 ms request can produce.

Nothing restores the previously read bytes, and
sit9531x_dpll_output_pin_phase_adjust_set() returns the error without
updating dpin->phase_adjust, so software keeps the old value while the device
holds the mixed one.  Would writing the arming byte last, or saving and
restoring the five original bytes before commit, bound this?

> +	if (ret && !rc)
> +		rc = ret;
> +	if (rc)
> +		return rc;
> +
> +	/*
> +	 * Restart the output divider phase so the freshly programmed delay is
> +	 * applied against a known edge instead of the divider's arbitrary
> +	 * running phase.
> +	 */
> +	return sit9531x_output_phase_flush(sitdev, pll_idx);
> +}
> +
>  /*
>   * sit9531x_clear_notifications - clear all notification registers
>   *
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index 6365a83e4c52..c07fb2256510 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -736,12 +736,63 @@ 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 cached value.
> + */
> +static int
> +sit9531x_dpll_output_pin_phase_adjust_get(const struct dpll_pin *pin,
> +					  void *pin_priv,
> +					  const struct dpll_device *dpll,
> +					  void *dpll_priv, s32 *phase_adjust,
> +					  struct netlink_ext_ack *extack)
> +{
> +	struct sit9531x_dpll_pin *dpin = pin_priv;
> +
> +	*phase_adjust = dpin->phase_adjust;
> +	return 0;
> +}

[Severity: Medium]
Does this match the commit message claim that "what the core reads back is
what the registers hold rather than what was asked for"?  The getter never
touches PROG6..PROG2; it returns the cache, and the setter below stores the
raw request into that cache.

sit9531x_output_phase_adjust_set() floors the coarse count and rounds and
clamps the residue, so the register value can differ from the request.  For
+10 ps at Fvco 5 GHz:

	coarse = mul_u64_u64_div_u64(10, 5000000000, 1000000000000) = 0
	steps  = div64_u64(10 + 15, 30) = 0

so no delay at all is programmed, yet DPLL_A_PIN_PHASE_ADJUST reports 10.

The cache also starts at 0 (kzalloc_obj() in sit9531x_dpll_pin_alloc()) and
is never seeded from the delay registers, so an NVM-preloaded delay reads back
as 0.  With that stale 0 the core drops a request to clear it:

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

Would reading the registers back and returning the effective, quantised value
avoid both of these?

> +
> +/*
> + * sit9531x_dpll_output_pin_phase_adjust_set - set output phase adjustment
> + *
> + * Programs the per-output PRG_RST_DELAY registers for deterministic
> + * phase offset; see sit9531x_output_phase_adjust_set() in core.c.
> + */
> +static int
> +sit9531x_dpll_output_pin_phase_adjust_set(const struct dpll_pin *pin,
> +					  void *pin_priv,
> +					  const struct dpll_device *dpll,
> +					  void *dpll_priv, s32 phase_adjust,
> +					  struct netlink_ext_ack *extack)
> +{
> +	struct sit9531x_dpll_pin *dpin = pin_priv;
> +	struct sit9531x_dpll *sitdpll = dpll_priv;
> +	struct sit9531x_dev *sitdev = sitdpll->dev;
> +	int rc;
> +
> +	mutex_lock(&sitdev->multiop_lock);
> +	rc = sit9531x_output_phase_adjust_set(sitdev, dpin->id, phase_adjust);
> +	mutex_unlock(&sitdev->multiop_lock);
> +
> +	if (rc) {
> +		NL_SET_ERR_MSG(extack, "Phase adjust failed");
> +		return rc;
> +	}
> +
> +	dpin->phase_adjust = phase_adjust;
> +	return 0;
> +}
> +
>  static 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,
>  };

[Severity: Medium]
Now that output pins support phase adjustment, should
struct dpll_pin_properties::phase_gran be populated too?  A grep for
phase_gran under drivers/dpll/ matches only dpll_netlink.c and zl3073x/;
sit9531x_prop_pin_get() sets only the range:

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

Documentation/driver-api/dpll.rst says that when pin phase adjustment is
supported, minimal and maximal values and granularity shall be provided on
DPLL_CMD_PIN_GET via DPLL_A_PIN_PHASE_ADJUST_MIN/MAX/GRAN.  With phase_gran
left at 0 the core skips both the attribute and the validation:

drivers/dpll/dpll_netlink.c:dpll_msg_add_pin_props() {
	if (prop->phase_gran &&
	    nla_put_u32(msg, DPLL_A_PIN_PHASE_ADJUST_GRAN,
			prop->phase_gran))
}

drivers/dpll/dpll_netlink.c:dpll_pin_phase_adj_set() {
	if (pin->prop.phase_gran && phase_adj % (s32)pin->prop.phase_gran) {
}

Should phase_gran be set to SIT9531X_OUT_PRG_FINE_STEP_PS so userspace can
see the 30 ps quantum, as zl3073x does?

>  
>  const struct dpll_pin_ops *
> diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
> index 031c1a460d4a..91d33abd15c3 100644
> --- a/drivers/dpll/sit9531x/regs.h
> +++ b/drivers/dpll/sit9531x/regs.h
> @@ -189,6 +189,32 @@
>  #define SIT9531X_REG_OUTSYS_DEBUG		SIT9531X_REG(0x03, 0xBD)
>  #define SIT9531X_DEBUG_UNLOCK_VAL		0xC3
>  
> +/*
> + * Per-output programmable phase delay: 34-bit coarse (in VCO clock
> + * cycles) plus a 3-bit fine field with fixed 30 ps steps.  Each output
> + * has a five-byte block PROG6..PROG2:

[ ... ]

> + * Outputs 0-5 are on Page 3, outputs 6-11 on Page 4.  The block base
> + * within a page is 0x15 + 16 * (out_idx % 6).
> + */

[Severity: Low]
This isn't a bug, but the documented formula is in terms of the logical
output index while the code uses the physical slot:

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

With clkout_map_95317[] = {0, 3, 4, 5, 7, 8, 9, 11}, out_idx 4 gives page 3 /
base 0x55 by the comment but page 4 / base 0x25 in the code.  The same
out_idx wording appears in the new block comment above
sit9531x_output_phase_adjust_set() in core.c.  Could both comments be reworded
in terms of the slot, matching the inline note "Page/base must address the
slot, not the logical index"?

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

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

* Re: [PATCH net-next v8 13/15] dpll: sit9531x: add support to get fractional frequency offset
  2026-09-02 21:40 ` [PATCH net-next v8 13/15] dpll: sit9531x: add support to get fractional frequency offset Ali Rouhi
@ 2026-09-08 12:45   ` netdev-bot+sashiko
  0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-09-08 12:45 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, 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: 0 · Medium: 2 · Low: 1

- [Medium] sit9531x_dpll_input_pin_ffo_get()
  (drivers/dpll/sit9531x/dpll.c:547) gates the new fractional-frequency-…
- [Medium] sit9531x_dpll_input_pin_ffo_get()
  (drivers/dpll/sit9531x/dpll.c:547) gates the measurement on the…
- [Low] The patch adds a stale documentation block headed
  'sit9531x_dpll_input_pin_phase_offset_get - read phase offset ...…

--

dpll: sit9531x: add support to get fractional frequency offset

This adds a fractional frequency offset getter for input pins, derived
from the difference between the DIVN the digital loop is running (read
through the latched debug window) and the DIVN in the configuration
registers.  The result is reported in parts per trillion through the
DPLL netlink pin attribute.

A few questions below about the gating of the new measurement and about
one of the added comment blocks.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index f552a9c73796..bd251ab60eee 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c

[ ... ]

> @@ -998,6 +1053,102 @@ static int sit9531x_divn_static(struct sit9531x_dev *sitdev, u8 pll_idx,

[ ... ]

> +/**
> + * sit9531x_pll_ffo_ppt - fractional frequency offset of a PLL's reference
> + * @sitdev:	device pointer
> + * @pll_idx:	PLL index (0-3)
> + * @ffo:	result in parts per trillion
> + *
> + * A locked PLL commands whatever DIVN keeps its VCO tracking the
> + * reference.  How far that sits from the configured DIVN is how far the
> + * reference sits from the local oscillator, which is the fractional
> + * frequency offset the DPLL ABI reports for the pin feeding the device.
> + *
> + * Caller must hold sitdev->multiop_lock.
> + *
> + * Return: 0 on success, -ENODATA when DIVN is not programmed, <0 on
> + * error.
> + */
> +int sit9531x_pll_ffo_ppt(struct sit9531x_dev *sitdev, u8 pll_idx, s64 *ffo)
> +{
> +	s64 configured, running, delta;
> +	u64 magnitude;
> +	int rc;
> +
> +	lockdep_assert_held(&sitdev->multiop_lock);
> +
> +	if (pll_idx >= SIT9531X_NUM_PLLS)
> +		return -EINVAL;
> +
> +	rc = sit9531x_divn_static(sitdev, pll_idx, &configured);
> +	if (rc)
> +		return rc;
> +	if (configured <= 0)
> +		return -ENODATA;

[ ... ]

> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index f813126077e1..1a14255e89a8 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -528,6 +528,52 @@ sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
>  	return 0;
>  }
>  
> +/*
> + * sit9531x_dpll_input_pin_phase_offset_get - read phase offset
> + *
> + * reads the TDC (Time-to-Digital Converter) hardware
> + * to measure the phase difference in picoseconds via
> + * sit9531x_phase_offset_read().
> + */

[Severity: Low]
This isn't a bug, but is this comment block a leftover?  It names
sit9531x_dpll_input_pin_phase_offset_get() and describes a TDC
picosecond reader, while the function that follows it is
sit9531x_dpll_input_pin_ffo_get(), which returns parts per trillion from
a DIVN ratio and never calls sit9531x_phase_offset_read().

sit9531x_dpll_input_pin_phase_offset_get() sits further down in the same
file and already carries its own, differently worded block starting with
"phase offset of a reference", so the file now has two doc blocks for
that one function.

On a related note, core.h (added by an earlier commit in the series)
files the new prototype under the phase-offset heading:

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

Would a separate heading for the frequency-offset helper be clearer?

> +/*
> + * sit9531x_dpll_input_pin_ffo_get - read the input's frequency offset
> + *
> + * The offset is derived from how far the PLL's running DIVN sits from
> + * its configured one, which only says something about the reference the
> + * PLL is actually tracking.  For every other input there is no
> + * measurement, and -ENODATA leaves the attribute out rather than
> + * reporting the active reference's figure against the wrong pin.
> + */
> +static int
> +sit9531x_dpll_input_pin_ffo_get(const struct dpll_pin *pin, void *pin_priv,
> +				const struct dpll_device *dpll, void *dpll_priv,
> +				struct dpll_ffo_param *ffo,
> +				struct netlink_ext_ack *extack)
> +{
> +	struct sit9531x_dpll_pin *dpin = pin_priv;
> +	struct sit9531x_dpll *sitdpll = dpll_priv;
> +	struct sit9531x_dev *sitdev = sitdpll->dev;
> +	int rc;
> +
> +	mutex_lock(&sitdev->multiop_lock);
> +
> +	/*
> +	 * The periodic worker updates selected_ref under the same lock, so
> +	 * test it here rather than before taking it: otherwise the
> +	 * reference can change in between and the measurement gets
> +	 * attributed to the wrong pin.
> +	 */

[Severity: Medium]
Does multiop_lock really give the guarantee described here?  The lock
serializes this read against the worker's update of the cached value,
but the entity that switches the reference is the device, which takes no
software lock.

chan->selected_ref is written only from the poll worker:

drivers/dpll/sit9531x/core.c:sit9531x_chan_state_fetch() {
	...
	chan->selected_ref =
		sit9531x_hw_src_input(input_sel & SIT9531X_PRIO_NIBBLE_MASK);
	...
}

and that worker runs every 500 ms:

drivers/dpll/sit9531x/core.c:sit9531x_dev_periodic_work() {
	...
	/* Run twice a second */
	kthread_queue_delayed_work(sitdev->kworker, &sitdev->work,
				   msecs_to_jiffies(SIT9531X_STATUS_POLL_MS));
}

So if the chip switches from IN0 to IN1 on its own, for up to one poll
period a DPLL_CMD_PIN_GET on IN0 passes the gate and the running-DIVN
sample, which now tracks IN1, is reported as IN0's offset, while IN1
returns -ENODATA and gets no attribute at all.

Would re-reading CLK_ACTIVESEL_PLL inside the same multiop_lock section
as the DIVN sampling close that window?  This still reads the same way
later in the series, so it does not look like a follow-up fixes it.

> +	if (sitdev->chan[sitdpll->id].selected_ref != dpin->id) {
> +		mutex_unlock(&sitdev->multiop_lock);
> +		return -ENODATA;
> +	}
> +
> +	rc = sit9531x_pll_ffo_ppt(sitdev, sitdpll->id, &ffo->ffo);

[Severity: Medium]
Should this gate also require the outer loop to be closed and locked?

selected_ref is derived from the input-select nibble alone, with no
regard for chan->mode, chan->locked, chan->inner_lol or chan->ho_freeze,
so a matching id does not imply the PLL is tracking that input.  The
driver's own definition of an active reference is stricter:

drivers/dpll/sit9531x/dpll.c:sit9531x_dpll_selection_state_get() {
	...
	active_input = !chan->mode && chan->locked && !chan->inner_lol &&
		       chan->selected_ref == pin_id;
	...
}

With only the selected_ref test, a free-running PLL (chan->mode == 1),
an unlocked one, or one in holdover (chan->ho_freeze) still reaches
sit9531x_pll_ffo_ppt(), which returns a value as soon as the configured
DIVN is non-zero:

	rc = sit9531x_divn_static(sitdev, pll_idx, &configured);
	if (rc)
		return rc;
	if (configured <= 0)
		return -ENODATA;

	rc = sit9531x_divn_runtime(sitdev, pll_idx, &running);
	...
	delta = running - configured;

In free-run that difference is not a reference offset at all, and in
holdover it is the frozen last correction, yet both are published
through DPLL_A_PIN_FRACTIONAL_FREQUENCY_OFFSET.

The comment above says the value "only says something about the
reference the PLL is actually tracking" - could the code enforce that
claim?  zl3073x_dpll_input_pin_ffo_get() returns -ENODATA unless
pin->operstate == DPLL_PIN_OPERSTATE_ACTIVE, and dpll_netlink.c already
treats -ENODATA as "omit the attribute", so the same shape is available
here.  This also appears unchanged later in the series.

> +	mutex_unlock(&sitdev->multiop_lock);
> +
> +	return rc;
> +}
> +

[ ... ]

> @@ -641,6 +687,7 @@ static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
>  	 * rather than a port rate.
>  	 */
>  	.supported_ffo		= BIT(DPLL_FFO_PIN_DEVICE),
> +	.ffo_get		= sit9531x_dpll_input_pin_ffo_get,
>  };

[ ... ]

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

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

* Re: [PATCH net-next v8 12/15] dpll: sit9531x: add support to get phase offset on the connected input pin
  2026-09-02 21:40 ` [PATCH net-next v8 12/15] dpll: sit9531x: add support to get phase offset on the connected input pin Ali Rouhi
@ 2026-09-08 12:45   ` netdev-bot+sashiko
  0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-09-08 12:45 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, 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: 0 · Medium: 4 · Low: 3

- [Medium] The TDC sample width is described three mutually inconsistent
  ways by artifacts this patch adds.
- [Medium] The callback's own comment states a hardware precondition it
  neither creates nor checks: "the TDC compares against a signal the PLL…
- [Medium] A transient I2C/regmap failure is converted into a fabricated,
  successful phase-offset measurement of 0.
- [Medium] The callback decides which pin owns the TDC sample from the
  software cache `sitdev->chan[sitdpll->id].selected_ref`, but takes a…
- [Low] The new kernel-doc for sit9531x_dpll_input_pin_phase_offset_get()
  justifies its entire design decision (report the closed-loop residual,…
- [Low] struct sit9531x_dpll_pin::phase_offset is declared `s64
  phase_offset; /* picoseconds */` (drivers/dpll/sit9531x/dpll.h:35),…
- [Low] sit9531x_phase_offset_read() documents the conversion as
  `phase_diff (ps) = tdc_code * 1e12 / fvco` but implements `fvco_mhz =…

--

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

This adds sit9531x_phase_offset_read() to sample the on-chip
time-to-digital converter through the PLL debug window and convert the
code to picoseconds, and wires it up as the input pin
.phase_offset_get callback.  Pins that are not the PLL's selected
reference report zero.

A few questions on the readback and on what gets reported to userspace.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index c93578f3b750..f552a9c73796 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -1586,6 +1586,131 @@ 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) 40-bit code from the
> + * PLL page registers, then converts to picoseconds using the VCO
> + * frequency: phase_diff = tdc_code / fvco.
> + *
> + * Caller must hold sitdev->multiop_lock.
> + */

[ ... ]

> +	tdc_raw = 0;
> +
> +	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
> +				  SIT9531X_PLL_REG_DBG_DATA_4, &v);
> +	if (rc)
> +		return rc;
> +	sign = !!(v & BIT(SIT9531X_TDC_SIGN_BIT));
> +	tdc_raw = (u64)(v & 0x07) << 32;

[Severity: Medium]
How wide is the TDC sample meant to be?  Three places in this patch
disagree.

The kernel-doc above says "Reads the Time-to-Digital Converter (TDC)
40-bit code".

regs.h annotates the top byte as nine bits in an eight bit register:

	#define SIT9531X_PLL_REG_DBG_DATA_4		0xB9  /* [39:32] + sign */

And the code keeps three bits of that register and takes bit 3 as the
sign (SIT9531X_TDC_SIGN_BIT == 3):

	sign = !!(v & BIT(SIT9531X_TDC_SIGN_BIT));
	tdc_raw = (u64)(v & 0x07) << 32;

That assembles code[34:0], not code[39:0].  If the register annotation is
the correct one, are magnitude bits [39:35] dropped here with no range
check, so a large sample is scaled and published as an arbitrary
DPLL_A_PIN_PHASE_OFFSET value instead of an error?

The 0x07 is also the only unnamed field mask in a header that names every
other field, which hides the width at the call site.  Could it get a
define next to SIT9531X_TDC_SIGN_BIT?

[ ... ]

> +	/*
> +	 * Get VCO frequency for conversion.  Fvco==0 means DIVN is not
> +	 * programmed (PLL unused on this board) -- skip silently rather
> +	 * than spamming the log on every poll cycle.
> +	 */
> +	fvco = sit9531x_get_fvco(sitdev, pll_idx);
> +	if (!fvco) {
> +		dev_dbg(sitdev->dev, "PLL%c: Fvco unknown, skip TDC\n",
> +			'A' + pll_idx);
> +		return -ENODEV;
> +	}

[Severity: Medium]
Is "Fvco==0 means DIVN is not programmed" the only case?  sit9531x_get_fvco()
also returns 0 on any register read failure:

	rc = sit9531x_divn_static(sitdev, pll_idx, &divn);
	if (rc || divn <= 0)
		return 0;

	doubler = sit9531x_is_xo_doubler_enabled(sitdev);
	if (doubler < 0)
		return 0;

So a single I2C NAK or timeout inside sit9531x_divn_static() ends up here
as -ENODEV, and the caller turns that into a successful measurement:

	if (rc == -ENODEV) {
		dpin->phase_offset = 0;
		*phase_offset = 0;
		return 0;
	}

Does that make a bus error indistinguishable from "the loop residual is
exactly 0 ps" on DPLL_A_PIN_PHASE_OFFSET?  The framework averages
successive samples, so every failing poll would pull the average toward
zero.

Note also the asymmetry within this same function: the identical bus
failure during the trigger and data reads is returned as an error, while
the one inside sit9531x_get_fvco() becomes a reported value.  Would
distinguishing "no DIVN programmed" from "read failed" (for example by
having sit9531x_get_fvco() return an errno separately from the value) keep
those two paths consistent?

> +
> +	/*
> +	 * phase_diff (seconds) = tdc_code / fvco
> +	 * phase_diff (ps) = tdc_code * 1e12 / fvco
> +	 *
> +	 * To avoid 64-bit overflow:
> +	 *   phase_ps = tdc_code * 1e6 / (fvco / 1e6)
> +	 */
> +	fvco_mhz = div64_u64(fvco, 1000000ULL);
> +	if (!fvco_mhz)
> +		return -EIO;
> +
> +	*phase_ps = div64_s64(tdc_signed * 1000000LL, (s64)fvco_mhz);

[Severity: Low]
The comment states the conversion as tdc_code * 1e12 / fvco, but the code
truncates the denominator to whole MHz first.

sit9531x_get_fvco() returns Hz derived from a fractional DIVN:

	return mul_u64_u64_div_u64(fref, (u64)divn, SIT9531X_DIVN_SCALE);

so a sub-MHz remainder is normal, and 2949120000 Hz becomes 2949 MHz here,
about 40 ppm of scale error on every reported magnitude.

Is the overflow concern real?  mul_u64_u64_div_u64() is already used twice
in this file (sit9531x_get_fvco() and sit9531x_pll_ffo_ppt()) and would
keep the exact Hz denominator:

	*phase_ps = mul_u64_u64_div_u64(abs(tdc_signed), 1000000000000ULL, fvco);

> +
> +	return 0;
> +}
> +
>  /*
>   * 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 76d7d7d21d3d..f813126077e1 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -528,6 +528,105 @@ 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

[ ... ]

> + * 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 behind a debugfs operation that owns
> + * the freeze and restores it; it is not this callback.

[Severity: Low]
Where is that debugfs operation?  Searching the driver directory at the tip
of the series, the only match for debugfs under drivers/dpll/sit9531x/ is
this comment line itself, and the directory contains only Kconfig, Makefile,
core.c/h, dpll.c/h, prop.c/h and regs.h.

Since this sentence is the justification for reporting the closed-loop
residual rather than the vendor's open-loop quantity, could it either point
at code that exists or be reworded so the rationale can be checked?

> + *
> + * 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 simply not meaningful", what stops it
from being reported as a measurement?

The callback only checks selected_ref and then reads unconditionally, and
sit9531x_phase_offset_read() returns 0 regardless of whether the PLL drives
any output, so whatever the debug window hands back is scaled and published
as DPLL_A_PIN_PHASE_OFFSET with no extack warning.

regs.h already defines SIT9531X_PLL_REG_ZDB0/ZDB1 with
SIT9531X_PLL_ZDB_EN_BIT and SIT9531X_PLL_REG_OUT_MAP_HI/LO.  Could the
getter test those and report zero (as it does for non-selected pins) when
the precondition does not hold?

The commit message does not mention the limitation at all; it says the
small value "is the measurement, not an artefact of it".  Could the
restriction be described there as well, so it is visible to anyone reading
git history?

[ ... ]

> +	mutex_lock(&sitdev->multiop_lock);
> +
> +	/*
> +	 * The on-chip TDC is a per-PLL resource that always measures the
> +	 * phase difference between the VCO and the PLL's currently
> +	 * selected reference; it cannot be pointed at an arbitrary input.
> +	 * For any input that is not the active reference there is no
> +	 * meaningful per-pin phase offset, so report 0 instead of the
> +	 * active reference's value.
> +	 */
> +	if (sitdev->chan[sitdpll->id].selected_ref != dpin->id) {
> +		mutex_unlock(&sitdev->multiop_lock);
> +		dpin->phase_offset = 0;
> +		*phase_offset = 0;
> +		return 0;
> +	}
> +
> +	rc = sit9531x_phase_offset_read(sitdev, sitdpll->id, &offset);
> +	mutex_unlock(&sitdev->multiop_lock);

[Severity: Medium]
Can the pin the sample is attributed to differ from the pin the TDC
actually measured?

The ownership test uses the software cache chan[].selected_ref, whose only
writer is sit9531x_chan_state_fetch() running from the periodic monitor
work / threaded IRQ:

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

The sample itself is live and always taken against whatever reference the
PLL has selected at read time.  multiop_lock serialises access to the cache
but does not stop the chip from switching reference on its own (LOS on the
active input, revertive switchback).

In the window between such a switch and the next cache refresh, would a
dpll pin get on the previously selected pin pass the
selected_ref == dpin->id test and publish the new reference's phase as the
old pin's DPLL_A_PIN_PHASE_OFFSET, while the pin that is genuinely selected
fails the test and reports exactly 0?

Would reading the active-select register inside this same multiop_lock
section, as sit9531x_chan_state_fetch() does, 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;
> +	}
> +
> +	/*
> +	 * 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;

[Severity: Low]
The value cached here is in ABI units (1/1000 ps), but the field it is
stored in is documented as picoseconds:

	s32				phase_adjust;	/* picoseconds */
	s64				phase_offset;	/* picoseconds */

The sibling phase_adjust really does hold picoseconds, since
DPLL_A_PIN_PHASE_ADJUST has no divider, so the two identical unit comments
now differ by a factor of 1000.  Nothing reads dpin->phase_offset today,
but would it be worth either caching the unscaled value or fixing the
comment to say 1/DPLL_PHASE_OFFSET_DIVIDER picoseconds?

> +	*phase_offset = offset;
> +	return 0;
> +}
> +

[ ... ]

> diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
> index 251c98dc7cbe..aa60be243e6d 100644
> --- a/drivers/dpll/sit9531x/regs.h
> +++ b/drivers/dpll/sit9531x/regs.h
> @@ -256,6 +256,42 @@

[ ... ]

> +#define SIT9531X_PLL_REG_DBG_DATA_3		0xB8  /* [31:24] */
> +#define SIT9531X_PLL_REG_DBG_DATA_4		0xB9  /* [39:32] + sign */
> +#define SIT9531X_PLL_REG_DBG_DATA_5		0xBA  /* [47:40] */

[ ... ]

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

These are the defines referenced in the sample-width question above.

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

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

* Re: [PATCH net-next v8 14/15] dpll: sit9531x: model the inter-PLL sync net as a pair of pins
  2026-09-02 21:40 ` [PATCH net-next v8 14/15] dpll: sit9531x: model the inter-PLL sync net as a pair of pins Ali Rouhi
@ 2026-09-08 12:45   ` netdev-bot+sashiko
  0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-09-08 12:45 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, 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: 1 · Medium: 4 · Low: 1 (1)

- [High] The INTSYNC net owner is tracked only by the software field
  sitdev->intsync_src, which is never reconciled with the hardware.
- [Medium] The commit message claims this patch models INTSYNC as a pair
  of pins, but the model and the pin registration already exist in the…
- [Medium] The new INTSYNC destination pin reports the hardware's
  automatic selection through DPLL_A_PIN_STATE instead of preserving the…
- [Medium] A partially failed disable leaves the cached owner naming a
  PLL that no longer drives the net, and the natural retry is a silent…
- [Medium] sit9531x_intsync_enable()'s err_disable label calls
  sit9531x_intsync_disable() and discards its return code…
- [Low] sit9531x_dpll_intsync_src_state_on_dpll_set() rejects
  DPLL_PIN_STATE_SELECTABLE with -EINVAL from the default case as…

Pre-existing issues:
- [Low] The new INTSYNC destination ops expose the generic priority
  getter, which reports an out-of-contract priority value when the…

--

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

This adds the register sequences that enable and disable the chip's
internal inter-PLL sync net (INTSYNC), plus two specialized dpll_pin_ops
tables: an output pin on the PLL that drives the net and an input pin on
each PLL that can select it.  Ownership of the net is tracked in the
software field sitdev->intsync_src.

No Link: tags in the commit header.

A few questions below about the ownership tracking and the failure paths.

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index bd251ab60eee..c3d7c4851549 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -1686,6 +1686,160 @@ 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_enable(struct sit9531x_dev *sitdev, u8 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;

[ ... ]

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

[ ... ]

> +	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.
> +	 */
> +	sit9531x_intsync_disable(sitdev, src_pll_idx);
> +
> +	return rc;
> +}

[Severity: Medium]
The comment says nothing else will ever clear the global bit, but the
rollback's own return code is discarded here.  Can the bit be stranded when
the rollback itself fails?

err_disable is only reached because a register access on this I2C bus just
failed, and sit9531x_intsync_disable() begins with two accesses on the same
bus:

	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;

If either fails, the global enable bit is still set on return.  The caller in
sit9531x_dpll_intsync_src_state_on_dpll_set() records the owner only on
success:

		rc = sit9531x_intsync_enable(sitdev, sitdpll->id);
		if (!rc)
			sitdev->intsync_src = sitdpll->id;

so intsync_src stays -1, and the DISCONNECTED branch short-circuits on
"if (sitdev->intsync_src != sitdpll->id) break;".  Since these two helpers
are the only writers of that bit and both are reached only through paths
keyed on intsync_src, is there any path left that can clear it?

A later CONNECTED request on a different PLL then passes the
"if (sitdev->intsync_src >= 0)" gate and programs that PLL's EXT page while
the first PLL's EXT page still holds the partially applied enable values
(0x2D/0x50/0x51/0x54/0x55/0x5C/0x5D/0x6C), so two PLLs end up configured
toward the same single-driver net.  Would propagating the rollback's error,
or recording the attempted owner, make that recoverable?

Separately, this rollback is not a restore: it writes the fixed dis_val to
all eight intsync_config entries, including the ones the aborted enable
never touched (0x20 to offset 0x55, 0x00 to 0x5C/0x5D/0x6C).  A failure at
i == 0 therefore overwrites eight EXT-page registers that still held their
power-on/NVM values with hardcoded constants.  Is that intended?

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

[ ... ]

> +	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &val);
> +	if (rc)
> +		return rc;
> +	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL,
> +			       val & ~BIT(SIT9531X_INTSYNC_EN_BIT));
> +	if (rc)
> +		return rc;
> +
> +	/* Small update on Page 0 */
> +	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
> +			       SIT9531X_SMALL_UPDATE_CMD);
> +	if (rc)
> +		return rc;
> +
> +	/* Unlock debug on EXT page */
> +	rc = sit9531x_write_u8(sitdev, SIT9531X_REG(ext_page, SIT9531X_PLL_REG_DEBUG),
> +			       SIT9531X_PLL_DEBUG_UNLOCK);
> +	if (rc)
> +		return rc;
> +
> +	for (i = 0; i < ARRAY_SIZE(intsync_config); i++) {
> +		rc = sit9531x_write_u8(sitdev,
> +				       SIT9531X_REG(ext_page,
> +						    intsync_config[i].offset),
> +				       intsync_config[i].dis_val);
> +		if (rc)
> +			return rc;
> +	}

[Severity: Medium]
The authoritative enable bit is cleared and committed by the first three
accesses here, then roughly ten more accesses follow, each returning early
with no rollback.  What state is the driver in if one of the later ones
fails?

The net is already off in hardware, but the caller keeps the cached owner:

	case DPLL_PIN_STATE_DISCONNECTED:
		if (sitdev->intsync_src != sitdpll->id)
			break;
		rc = sit9531x_intsync_disable(sitdev, sitdpll->id);
		if (!rc)
			sitdev->intsync_src = -1;

So intsync_src still names this PLL while it no longer drives the net.
sit9531x_dpll_intsync_src_state_on_dpll_get() then reports CONNECTED,
sit9531x_dpll_intsync_dst_state_on_dpll_get() reports DISCONNECTED for the
same PLL, and sit9531x_dpll_intsync_dst_state_on_dpll_set() refuses
SELECTABLE with "PLL cannot lock to the INTSYNC it drives".

Re-issuing CONNECTED on that PLL cannot repair it either, because the fast
path "if (sitdev->intsync_src == sitdpll->id) break;" returns 0 without
touching hardware, and every other PLL is refused with -EBUSY.  Is a second
DISCONNECTED really the only way to converge, and would clearing the cache
unconditionally (or on the first committed write) be preferable?

> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index 1a14255e89a8..993a991d5b25 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c

[ ... ]

> @@ -717,8 +731,214 @@ 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;
> +}

[Severity: High]
Is intsync_src ever reconciled with the hardware?  sit9531x_dev_probe() does

	sitdev->intsync_src = -1;

and grep shows SIT9531X_REG_INTSYNC_GLOBAL is read only inside
sit9531x_intsync_enable() and sit9531x_intsync_disable(), so neither probe,
sit9531x_dev_state_fetch() nor the periodic poller samples it.
sit9531x_i2c_driver also defines only .probe, with no .remove or .shutdown,
so an enable done through the new source pin stays programmed in the chip
across unbind.

Concrete sequence: set the SYNCOUT pin CONNECTED on PLL A (hardware enabled,
intsync_src == 0), then unbind and rebind the i2c device.  The chip is still
driving the net while probe forces intsync_src back to -1.  In that state:

- this getter reports DISCONNECTED on all four DPLLs while the net is driven;

- a CONNECTED request on PLL B passes

		if (sitdev->intsync_src >= 0) {

  and programs B's EXT page while A's EXT page still drives the same net;

- a DISCONNECTED request on the real source hits

		if (sitdev->intsync_src != sitdpll->id)
			break;

  and returns success without touching a register;

- the mirror guard "PLL cannot lock to the INTSYNC it drives" no longer
  applies to the real source, so SRC_INTSYNC can be added to the driving
  PLL's own priority table.

The same divergence appears at first probe on boards whose efuse/NVM blob
boots with an INTSYNC PLL already configured, which the driver documents
elsewhere in core.c ("some configs (e.g. an INTSYNC PLL) run a VCO that
Fref*DIVN does not reproduce"), since probe deliberately never resets the
chip.  Could intsync_src be seeded from the global enable bit plus the
per-PLL EXT configuration at probe (or the disable sequence run once at
init), and INTSYNC disabled at teardown?

The core.h comment describes the field as "PLL index currently sourcing
inter-PLL synchronization (INTSYNC), or -1 when disabled", which is what the
code does not currently guarantee.

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

[ ... ]

> +	default:
> +		rc = -EINVAL;
> +		break;
> +	}
> +
> +	mutex_unlock(&sitdev->multiop_lock);
> +
> +	if (rc && rc != -EBUSY)
> +		NL_SET_ERR_MSG(extack, "Failed to set INTSYNC source state");
> +
> +	return rc;
> +}

[Severity: Low]
A SELECTABLE request is a documented policy rejection returning -EINVAL from
the default case, but this filter only excludes -EBUSY, so userspace gets the
hardware-failure text "Failed to set INTSYNC source state" for it.  Should
the policy errno be excluded here as the two peer setters in this file do?

sit9531x_dpll_intsync_dst_state_on_dpll_set() uses

	else if (rc && rc != -EINVAL && rc != -EOPNOTSUPP)

and sit9531x_dpll_input_pin_state_on_dpll_set() uses

	else if (rc && rc != -EOPNOTSUPP && rc != -EINVAL)

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

[Severity: Medium]
Should the hardware's current selection be reported through
DPLL_A_PIN_OPERSTATE rather than DPLL_A_PIN_STATE here?

Documentation/driver-api/dpll.rst says "Pin state (DPLL_A_PIN_STATE)
reflects the administrative intent set by the user. Pin operational state
(DPLL_A_PIN_OPERSTATE) reflects what the hardware is actually doing with the
pin", and in automatic mode the user may request only SELECTABLE or
DISCONNECTED.

The setter honours that (it rejects CONNECTED with -EOPNOTSUPP), but this
getter delegates to sit9531x_dpll_selection_state_get(), which returns
CONNECTED when

	active_input = !chan->mode && chan->locked && !chan->inner_lol &&
		       chan->selected_ref == pin_id;

So a user sets SELECTABLE and reads back CONNECTED, a value the setter
refuses, and autonomous re-selection generates state notifications for
unchanged administrative intent.  sit9531x_dpll_intsync_dst_pin_ops also has
no .operstate_on_dpll_get, so the actual-selection information has no
attribute of its own.

zl3073x_dpll_input_pin_state_on_dpll_get() returns SELECTABLE in AUTO mode
and reports the active reference from
zl3073x_dpll_input_pin_operstate_on_dpll_get() instead.

> +
> +/*
> + * sit9531x_dpll_intsync_dst_state_on_dpll_set - lock a PLL to INTSYNC

[ ... ]

> +	case DPLL_PIN_STATE_CONNECTED:
> +		NL_SET_ERR_MSG(extack,
> +			       "Device selects its reference by priority; use selectable");
> +		rc = -EOPNOTSUPP;
> +		break;

[ ... ]

> +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: Low]
This isn't a bug introduced by this patch, but reusing the generic priority
getter for the new pin also inherits its out-of-contract return value when
the source is absent from the PLL's priority table.

sit9531x_input_prio_get() uses an in-band sentinel:

	*prio = SIT9531X_PRIO_MAX_SLOTS;
	return 0;

and sit9531x_dpll_input_pin_prio_get() passes it on as a successful result:

	dpin->prio = slot;
	*prio = slot;
	return 0;

so DPLL_A_PIN_PRIO reports 11 for a disconnected INTSYNC destination, while
sit9531x_dpll_input_pin_prio_set() rejects that value:

	if (prio >= SIT9531X_PRIO_MAX_SLOTS) {
		NL_SET_ERR_MSG(extack, "Priority out of range (0-10)");
		return -EINVAL;
	}

The sentinel-leaking getter came in with the earlier priority patch in this
series and applies to all physical input pins as well.

> @@ -1044,8 +1264,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;

[Severity: Medium]
The commit message says the two ends "are two pins rather than one: an
output pin on the source and an input pin on each destination", but is that
model actually introduced here?

At the parent revision core.h already defines

	#define SIT9531X_INTSYNC_PIN_ID		(SIT9531X_MAX_INPUTS + 1)
	#define SIT9531X_INTSYNC_OUT_PIN_ID	SIT9531X_MAX_OUTPUTS

and sit9531x_dpll_pin_is_registrable() already returns true for both:

	if (dir == DPLL_PIN_DIRECTION_INPUT) {
		/* The internal INTSYNC and XO pins are always registrable */
		if (index == SIT9531X_INTSYNC_PIN_ID ||
		    index == SIT9531X_MAX_INPUTS)
			return true;

What this patch adds is the dispatch above.  Without it, the destination pin
gets sit9531x_dpll_input_pin_ops and the source pin gets
sit9531x_dpll_output_pin_ops, and those handlers index arrays by pin id:

	struct sit9531x_ref *ref = &sitdev->ref[dpin->id];

	actual_pll = sitdev->out[dpin->id].pll_idx;

ref[] is sized SIT9531X_MAX_INPUTS + 1 and out[] is sized
SIT9531X_MAX_OUTPUTS, so both INTSYNC ids are one past the end.  The new
comment above sit9531x_dpll_intsync_dst_pin_ops notes the same thing ("one
past the end of the ref[] array").

Does that mean every bisect point between the pin-registration patch and
this one lets a DPLL_CMD_PIN_SET on the INTSYNC pins read past ref[] or
out[]?  Would reordering or squashing the registration and the specialized
ops avoid that, and could the message say it adds the ops for an already
registered pair rather than introducing the model?

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

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

* Re: [PATCH net-next v8 15/15] dpll: sit9531x: allow the device tree to override two board facts
  2026-09-02 21:40 ` [PATCH net-next v8 15/15] dpll: sit9531x: allow the device tree to override two board facts Ali Rouhi
@ 2026-09-08 12:45   ` netdev-bot+sashiko
  0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-09-08 12:45 UTC (permalink / raw)
  To: arouhi
  Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh,
	krzk+dt, conor+dt, cjubran, 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] sit9531x_parse_board_config()
  (drivers/dpll/sit9531x/core.c:3193-3195) copies the DT array…
- [Medium] Every failure path in the new sit9531x_parse_board_config()
  (drivers/dpll/sit9531x/core.c:3166-3196) is silent, so a board that…
- [Medium] The new parser accepts "sitime,pll-fvco" values with no
  plausibility check (drivers/dpll/sit9531x/core.c:3171-3172),…
- [Low] The commit message's justification for the new "sitime,pll-fvco"
  DT property does not match the code it activates.

--

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

The patch adds sit9531x_parse_board_config(), which reads two optional
vendor properties at probe time: "sitime,pll-fvco" (per-PLL VCO frequency)
and "sitime,output-pll-map" (per-output source PLL).  Absent both
properties the driver keeps deriving Fvco from DIVN and reading the routing
from the OUT_MAP registers.

A few questions below, one about the changelog and three about the new
parser.

> dpll: sit9531x: allow the device tree to override two board facts
>
>     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.

[Severity: Low]
Is the free-run rationale here consistent with the code the property feeds?

The kernel-doc above sit9531x_get_fvco() says the opposite about free-run:

  * Fvco = Fref * DIVN, where DIVN comes from sit9531x_divn_static() and
  * Fref = xtal_freq << doubler.  DIVN is the
  * steady-state Fvco/Fref target programmed by the NVM blob and is
  * authoritative in both free-run and sync modes; ...

The override's own comment in sit9531x_get_fvco() and the binding text name
INTSYNC, not free-run, as the case where the derivation does not reproduce
the VCO:

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

Could the changelog name the configuration where Fref*DIVN actually fails?

The changelog also says "Absent the properties the driver behaves exactly as
before", which is accurate, but it does not mention that when the property
is present sit9531x_output_freq_set() stops clamping Fvco to the per-PLL
band:

	} else if (!sitdev->pll_fvco[pll_idx]) {
		if (fvco < fvco_min)
			fvco = fvco_min;
		else if (fvco > fvco_max)
			fvco = fvco_max;
	}

Should that clamp bypass be called out, given this adds a permanent
vendor-specific DT ABI?

> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index c3d7c4851549..d75ac02047b5 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -3158,6 +3158,43 @@ static u64 sit9531x_derive_clock_id(struct sit9531x_dev *sitdev)
>  	return clkid;
>  }
>  
> +/*
> + * Board-config overrides for fixed efuse/blob routing the chip registers do
> + * not describe unambiguously.  Absent properties leave pll_fvco[] zeroed
> + * (derive from DIVN) and out_pll_map_valid false (use the OUT_MAP registers).
> + */
> +static void sit9531x_parse_board_config(struct sit9531x_dev *sitdev)
> +{
> +	u32 map[SIT9531X_MAX_OUTPUTS];
> +	int n, i;
> +
> +	device_property_read_u64_array(sitdev->dev, "sitime,pll-fvco",
> +				       sitdev->pll_fvco, SIT9531X_NUM_PLLS);

[Severity: Medium]
Should the return value of this read be checked?

device_property_read_u64_array() requires exactly SIT9531X_NUM_PLLS
elements, so a property with 1..3 entries returns -EOVERFLOW and writes
nothing.  pll_fvco[] then stays zero and sit9531x_get_fvco() silently falls
back to the derivation:

	if (pll_idx < SIT9531X_NUM_PLLS && sitdev->pll_fvco[pll_idx])
		return sitdev->pll_fvco[pll_idx];

A board that states its VCO incorrectly is therefore indistinguishable from
a board that states nothing, with nothing in dmesg.  Would a dev_warn() (or
dev_err_probe()) on a non-zero return be preferable?

[Severity: Medium]
Can an out-of-range Fvco here silently misprogram DIVO?

The value is stored without any plausibility check, sit9531x_get_fvco()
returns it verbatim, and sit9531x_output_freq_set() skips the band clamp
for it, so fvco is no longer bounded by SIT9531X_FVCO_HIGHBAND_MAX.  DIVO
is then only checked for zero:

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

but it is packed into a 34-bit field with the upper bits masked away:

	divo_bytes[4] = (divo >> 32) & 0x03;  /* only bits [1:0] */

Before this patch fvco was always clamped to <= 7.8125 GHz, so divo stayed
below 2^34 for any frequency >= 1 Hz and the mask could not lose anything.
With an unclamped DT Fvco (say a units typo giving 69 GHz) and a low output
rate reachable through the esync path, divo >= 2^34 is truncated, written,
and committed by sit9531x_prg_commit() with a phase flush, while
sitdev->out[out_idx].freq is cached from the untruncated divo and the call
returns success.

Would bounding the DT Fvco (and adding minimum/maximum to the schema, which
currently states the bands only in prose), or rejecting divo >= BIT_ULL(34)
instead of truncating, close that?

> +
> +	if (!device_property_present(sitdev->dev, "sitime,output-pll-map"))
> +		return;
> +
> +	/*
> +	 * Any 1..MAX_OUTPUTS length is accepted so the 8-output SiT95317 need
> +	 * not pad to 12; variant detection has not run yet and entries past
> +	 * the detected num_outputs are never indexed.  Trailing entries of a
> +	 * short map must read as unmapped rather than 0 (== PLLA), which
> +	 * would mark unrouted outputs active in sit9531x_out_state_fetch().
> +	 */
> +	memset(sitdev->out_pll_map, SIT9531X_OUT_PLL_UNMAPPED,
> +	       sizeof(sitdev->out_pll_map));
> +
> +	n = device_property_count_u32(sitdev->dev, "sitime,output-pll-map");
> +	if (n <= 0 || n > SIT9531X_MAX_OUTPUTS ||
> +	    device_property_read_u32_array(sitdev->dev, "sitime,output-pll-map",
> +					   map, n))
> +		return;

Related to the previous comment on silent failures: all three rejection
cases here return with out_pll_map_valid still false and no diagnostic, so
sit9531x_out_state_fetch() goes back to the OUT_MAP registers the board just
declared wrong.

Since the parse runs before sit9531x_read_variant_id() and
sit9531x_match_variant(), n also cannot be compared against
info->num_outputs.  A 12-output SiT95316 node carrying only 8 entries is
accepted, outputs 8..11 stay SIT9531X_OUT_PLL_UNMAPPED, and four DPLL pins
never appear to userspace even though the binding's minItems: 12
conditional forbids that DT.  Would moving the parse after variant
detection let the length be validated?

> +
> +	for (i = 0; i < n; i++)
> +		sitdev->out_pll_map[i] = map[i];
> +	sitdev->out_pll_map_valid = true;
> +}

[Severity: Medium]
Should the element values be validated before they are narrowed into the u8
out_pll_map[]?

The binding states the values exhaustively:

    items:
      enum: [0, 1, 2, 3, 255]

and the kernel-doc of struct sit9531x_dev repeats it:

 * @out_pll_map:	optional per-output source PLL (0-3, 0xff =
 *			unmapped) from DT "sitime,output-pll-map"

Neither is enforced at runtime, and out_pll_map_valid is set
unconditionally.  Two results follow in sit9531x_out_state_fetch():

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

An entry in 4..254 becomes "unmapped", so an output that is not muted loses
its DPLL pin and userspace can no longer see, mute or retune a clock that is
actively driving.

An entry >= 0x100 whose low byte is 0..3 (0x100, say) truncates to a
valid-looking index, passes m < SIT9531X_NUM_PLLS, and marks the output
routed on a PLL that does not drive it.  That index is then the driver's
only routing model: it picks the VCO band in sit9531x_output_freq_set()

	if (pll_idx == 1 || pll_idx == 3) {
		/* PLLB, PLLD: high band */
		fvco_min = SIT9531X_FVCO_HIGHBAND_MIN;

and selects the PLL passed to sit9531x_output_phase_flush(), which disturbs
every output of that unrelated PLL.

Isn't that the same failure the 0xFF pre-fill comment above says it
prevents ("would mark unrouted outputs active in
sit9531x_out_state_fetch()")?  Would rejecting, or storing as
SIT9531X_OUT_PLL_UNMAPPED with a warning, any entry that is neither
< SIT9531X_NUM_PLLS nor SIT9531X_OUT_PLL_UNMAPPED cover both cases?

[ ... ]

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

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

* Re: [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver
  2026-09-02 21:40 [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (14 preceding siblings ...)
  2026-09-02 21:40 ` [PATCH net-next v8 14/15] dpll: sit9531x: model the inter-PLL sync net as a pair of pins Ali Rouhi
@ 2026-09-08 12:57 ` Paolo Abeni
  15 siblings, 0 replies; 31+ messages in thread
From: Paolo Abeni @ 2026-09-08 12:57 UTC (permalink / raw)
  To: Ali Rouhi
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
	conor+dt, cjubran, Oleg.Zadorozhnyi, devicetree, netdev,
	linux-kernel, jiri

On 9/2/26 11:40 PM, Ali Rouhi wrote:
> 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/
> 
> 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 introduces it feature by feature instead:
> 
>   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 was built on its own, with W=1 on
> x86_64 and i386, and is sparse clean. Each one also links: no patch
> calls something that a later patch introduces, so the module builds at
> every step of the series and a bisect cannot land on a tree that fails
> to compile.
> 
> The split makes no functional change of its own. The tree this series
> ends at compiles to objects byte-identical to the tree the single patch
> produced once the fixes below are applied to it.
> 
> Changes in v8:
> 
>   - Split the driver patch into the thirteen patches listed above.
> 
>   - Advertise automatic mode only. v7 mapped DPLL_MODE_MANUAL onto the
>     chip's free-run bit and then refused DPLL_PIN_STATE_CONNECTED on
>     input pins, so it offered the mode whose one purpose is manual
>     selection and refused the only request that mode exists for.
>     Free-run selects no input at all, so on this device it is a lock
>     state, not a selection mode. v8 advertises AUTOMATIC only and
>     refuses anything else from .mode_set(), which now clears the outer
>     loop disable bit so a PLL left free-running by the loaded
>     configuration returns to selecting from the priority table.
>     Free-run is reported through lock status as UNLOCKED.
> 
>     The device can also pin one reference while the loop keeps running,
>     so real MANUAL support is possible later. It is left out here
>     because the mode mapping becomes ABI once merged, and it belongs in
>     the same series as the userspace that needs it rather than being
>     approximated now.
> 
>   - State the pin-state contract once, above the pin ops, and let the
>     two selection-role pins share one implementation of it. The driver
>     had five ops tables and five hand-written encodings of the same
>     state machine, and every review round has found another place where
>     two of them disagreed. This closes several findings at once:
>     CONNECTED reported while the outer loop was disabled, membership
>     taken from the shared-receiver refcount rather than from the
>     priority table, SELECTABLE on an INTSYNC destination userspace had
>     disconnected, .prio_set() inserting a source that was never in the
>     table, and .prio_remove() returning 0 when the disconnect would
>     have emptied it. The last two are now -EINVAL and -EBUSY.
> 
>   - Report LOCKED_HO_ACQ only once the holdover estimate is valid. It
>     was asserted whenever the loop was locked, without reading the bit
>     that says the device has accumulated a frequency estimate good
>     enough to fall back on. That bit is distinct from the one that says
>     the PLL has already switched over to holdover.
> 
>   - Fix the 32-bit build. The output phase adjust reduced a negative
>     request modulo the output period with % on two u64 values, leaving
>     the module with an undefined __umoddi3; it now uses div64_u64_rem(),
>     which the rest of the arithmetic there already uses. Reported by
>     Jakub Kicinski.
> 
>   - Correct the register model comment: the page selector is at 0xFF and
>     a page holds 256 registers, as the definitions right below the
>     comment already said. Reported by Ivan Vecera.
> 
>   - Type ordinary clock outputs as DPLL_PIN_TYPE_EXT instead of
>     DPLL_PIN_TYPE_GNSS, which claimed a GNSS-disciplined timing port.
>     Advertise a phase-adjust range on outputs only, and drop the input
>     pins' .phase_adjust_get with it: inputs have no .phase_adjust_set,
>     so a non-zero range promised something every set would refuse.
> 
>   - Always issue the loop lock in the programming commit, so a transient
>     I2C error cannot leave the chip in PRG_CMD with its loops open. Undo
>     the global enable when the INTSYNC enable fails partway. Take
>     multiop_lock before testing the selected reference in the frequency
>     offset getter, as the sibling phase-offset getter already does.
> 
>   - Latch every read of the chip's debug block the way the phase read
>     does. That block returns a sample of an internal measurement tap,
>     selected by a read code and latched by reading a trigger register.
>     The generic helper read the trigger once where the phase path reads
>     it three times, and a single read returns the previous latch. On
>     hardware this showed as a frequency offset that would not move while
>     a phase measurement of the same PLL tracked a temperature step
>     cleanly.
> 
>   - Report which PLLs came up and which EEPROM profile was loaded, and
>     warn when a PLL drives outputs without having reached its active
>     state, or when the profile's stored and computed CRCs disagree. In
>     both cases every register still reads back a plausible value, so
>     nothing about the failure is visible until the measurements come out
>     wrong.
> 
>   - Say what the phase offset getter measures, next to the callback: the
>     loop's residual error sampled with the loop closed, which is the
>     quantity the ABI asks for -- not the vendor's open-loop
>     phase-difference procedure, which would need the digital loop filter
>     frozen and would leave the PLL undisciplined until something
>     released it. A netlink read must not do that.
> 
>   - Drop fifteen register definitions that have never had a caller.
>     Eleven describe that open-loop sequence and the PLL restart path,
>     neither of which the driver performs. The other four -- the PLL page
>     macro and the three DIVN2 fields -- had no user in any patch of the
>     split, so there was no honest patch to introduce them in. Each will
>     come back with its first user.
> 
>   - Binding: put the example VCO frequency inside a band. 6251500000 sat
>     in the gap between the device's two bands, which the property's own
>     description rules out; it is now 6900000000, which a shipping
>     profile on our SiT95316 board runs. This is the only change to the
>     binding since v6 -- the property and its description are unchanged
>     -- so Krzysztof's Reviewed-by is carried across it.
> 
> The AI review's 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.
Clashiko had several comments, and some of them definitely look real.
Please note that you are supposed to handle such comments proactively,
see commit c82ff94592fb68f529afe63ca7f5ddb7dae4ba83

/P


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

end of thread, other threads:[~2026-09-08 12:57 UTC | newest]

Thread overview: 31+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-02 21:40 [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
2026-09-02 21:40 ` [PATCH net-next v8 01/15] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
2026-09-02 21:40 ` [PATCH net-next v8 03/15] dpll: add basic SiTime SiT9531x support Ali Rouhi
2026-09-08 12:45   ` netdev-bot+sashiko
2026-09-02 21:40 ` [PATCH net-next v8 02/15] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
2026-09-08 12:45   ` netdev-bot+sashiko
2026-09-02 21:40 ` [PATCH net-next v8 05/15] dpll: sit9531x: register DPLL devices and pins Ali Rouhi
2026-09-08 12:45   ` netdev-bot+sashiko
2026-09-02 21:40 ` [PATCH net-next v8 04/15] dpll: sit9531x: read DPLL types and pin properties from system firmware Ali Rouhi
2026-09-08 12:45   ` netdev-bot+sashiko
2026-09-02 21:40 ` [PATCH net-next v8 07/15] dpll: sit9531x: add support to get and set priority on input pins Ali Rouhi
2026-09-08 12:45   ` netdev-bot+sashiko
2026-09-02 21:40 ` [PATCH net-next v8 06/15] dpll: sit9531x: implement input pin state on a DPLL Ali Rouhi
2026-09-08 12:45   ` netdev-bot+sashiko
2026-09-02 21:40 ` [PATCH net-next v8 09/15] dpll: sit9531x: implement output " Ali Rouhi
2026-09-08 12:45   ` netdev-bot+sashiko
2026-09-02 21:40 ` [PATCH net-next v8 08/15] dpll: sit9531x: add support to get and set frequency on pins Ali Rouhi
2026-09-08 12:45   ` netdev-bot+sashiko
2026-09-02 21:40 ` [PATCH net-next v8 10/15] dpll: sit9531x: add support to adjust output phase Ali Rouhi
2026-09-08 12:45   ` netdev-bot+sashiko
2026-09-02 21:40 ` [PATCH net-next v8 11/15] dpll: sit9531x: add support to get and set esync on pins Ali Rouhi
2026-09-08 12:45   ` netdev-bot+sashiko
2026-09-02 21:40 ` [PATCH net-next v8 13/15] dpll: sit9531x: add support to get fractional frequency offset Ali Rouhi
2026-09-08 12:45   ` netdev-bot+sashiko
2026-09-02 21:40 ` [PATCH net-next v8 12/15] dpll: sit9531x: add support to get phase offset on the connected input pin Ali Rouhi
2026-09-08 12:45   ` netdev-bot+sashiko
2026-09-02 21:40 ` [PATCH net-next v8 15/15] dpll: sit9531x: allow the device tree to override two board facts Ali Rouhi
2026-09-08 12:45   ` netdev-bot+sashiko
2026-09-02 21:40 ` [PATCH net-next v8 14/15] dpll: sit9531x: model the inter-PLL sync net as a pair of pins Ali Rouhi
2026-09-08 12:45   ` netdev-bot+sashiko
2026-09-08 12:57 ` [PATCH net-next v8 00/15] dpll: add SiTime SiT9531x DPLL clock driver Paolo Abeni

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®