* [PATCH v3 net-next 1/3] dt-bindings: vendor-prefixes: add SiTime Corporation
2026-07-31 18:09 [PATCH v3 net-next 0/3] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
@ 2026-07-31 18:09 ` Ali Rouhi
2026-07-31 18:09 ` [PATCH v3 net-next 2/3] dt-bindings: dpll: add SiTime SiT9531x clock generator Ali Rouhi
2026-07-31 18:09 ` [PATCH v3 net-next 3/3] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
2 siblings, 0 replies; 4+ messages in thread
From: Ali Rouhi @ 2026-07-31 18:09 UTC (permalink / raw)
To: jiri@resnulli.us
Cc: vadim.fedorenko@linux.dev, arkadiusz.kubalewski@intel.com,
robh@kernel.org, krzk+dt@kernel.org, conor+dt@kernel.org,
cjubran@nvidia.com, Oleg.Zadorozhnyi@devoxsoftware.com,
devicetree@vger.kernel.org, netdev@vger.kernel.org,
linux-kernel@vger.kernel.org, Ali Rouhi, Conor Dooley
Add vendor prefix for SiTime Corporation, manufacturer of
programmable clock generators and MEMS oscillators.
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
Acked-by: Conor Dooley <conor.dooley@microchip.com>
---
Documentation/devicetree/bindings/vendor-prefixes.yaml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml
index 396044f368e7..1921718a5f82 100644
--- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
+++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
@@ -1535,6 +1535,8 @@ patternProperties:
description: SiRF Technology, Inc.
"^sis,.*":
description: Silicon Integrated Systems Corp.
+ "^sitime,.*":
+ description: SiTime Corporation
"^sitronix,.*":
description: Sitronix Technology Corporation
"^skov,.*":
--
2.43.0
This email message and any attachments are for the sole use of the intended recipient and contain proprietary and/or confidential information that may be privileged or otherwise protected from disclosure. Any unauthorized review, use, disclosure, copying or distribution of the information included in this message and any attachment is prohibited and may be unlawful. If you are not an intended recipient, please contact the sender by reply email and permanently delete and destroy the original message and any attachments, and any printouts or copies of the message and attachments to the original message. Thank you.
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH v3 net-next 0/3] dpll: add SiTime SiT9531x DPLL clock driver
@ 2026-07-31 18:09 Ali Rouhi
2026-07-31 18:09 ` [PATCH v3 net-next 1/3] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: Ali Rouhi @ 2026-07-31 18:09 UTC (permalink / raw)
To: jiri@resnulli.us
Cc: vadim.fedorenko@linux.dev, arkadiusz.kubalewski@intel.com,
robh@kernel.org, krzk+dt@kernel.org, conor+dt@kernel.org,
cjubran@nvidia.com, Oleg.Zadorozhnyi@devoxsoftware.com,
devicetree@vger.kernel.org, netdev@vger.kernel.org,
linux-kernel@vger.kernel.org, Ali Rouhi
This series adds a DPLL subsystem driver for the SiTime SiT95316 and
SiT95317 I2C clock generators. Each device integrates four PLLs with
automatic/manual reference selection, DCO frequency adjustment, 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/
Changes since v2:
- Scale the reported netlink phase offset by DPLL_PHASE_OFFSET_DIVIDER.
DPLL_A_PIN_PHASE_OFFSET carries 1/DPLL_PHASE_OFFSET_DIVIDER of a
picosecond; the driver passed the raw TDC picosecond value straight
through, so pin-get reported every reading a thousand times too small
(a measured -9151 ps appeared as -9.151 ps).
- Fix the kernel-doc of the state structs and getters (parameter names
and missing descriptions) so the series is clean under kernel-doc.
- Sync the cached PLL mode from the mode_set callback so a later
mode_get returns the value just set instead of a stale cache.
- Report a forced Hi-Z output as DPLL_PIN_STATE_DISCONNECTED so
state-get reflects what the pin actually drives.
- Manage the input pll_mask correctly: seed it from the hardware at
probe and set it only after prio_add succeeds, so disabling an input
shared by several DPLLs never cuts a reference still in use and a
failed claim does not leak.
- Commit the priority table even when a slot write returns an error,
so a partial failure cannot strand the chip in the PRG_CMD
programming state.
- Read input priority from the chip's Page-1 priority table instead of
a software default; pin-get previously reported priority 0 for every
input.
- Remove a DISCONNECTED input from the PLL priority table, not just the
receiver enable bits, so automatic mode can no longer keep selecting
it; reconnect re-adds it at the lowest priority.
- Refcount an input shared by several DPLLs; the physical receiver is
disabled only when the last DPLL releases it.
- Split INTSYNC into a source (output) pin and a destination (input)
pin so neither role overloads the other; the destination pin carries
priority ops and is polled for autonomous state changes.
- Expose the on-chip XO as a read-only DPLL_PIN_TYPE_INT_OSCILLATOR
pin, and read its status from the XO clock monitor rather than an
unrelated input-lane monitor.
- Constrain output-pll-map per variant in the binding (SiT95317 limited
to 8, SiT95316 requires all 12 when present) and default unspecified
trailing entries to unmapped (0xff).
- Acknowledge the notification latches inside the threaded IRQ handler
so the IRQF_ONESHOT unmask cannot trigger an interrupt storm.
- Deassert the reset GPIO with a boot delay before the first I2C access
so a board powered up in reset does not NACK the first read.
- Commit the phase-adjust write through the PRG_CMD/PHFL programming
sequence so the programmed delay lands against a defined edge.
- Program the pulse generator on an esync set; the embedded-sync marker
was advertised but never emitted.
- Restore the phase trigger source select after an output phase flush,
including on the pulse-write failure path, so a hardware sync input
is not left permanently hijacked.
- Wrap the PULSE_CTRL register write in the PRG_CMD programming state so
the pulse configuration latches standalone.
- Leave the PRG_CMD programming state on any output-system write
failure so the chip is not left with the output loops unlocked.
- Fix the implicit fractional denominator in the DCO helpers (default
2^32, 64-bit intermediates); reject out-of-range ppb.
- Restore the other PLLs' DCO mask after dco_adjust so their autonomous
tracking is not silently broken.
- Init monitoring work before DPLL allocation so the error path does
not cancel an uninitialized work.
- Propagate the holdover-release error and clear pin->dpll_pin on the
pin_get failure path so the free helper does not warn on an ERR_PTR.
Notes for reviewers (points raised in review that are intentional):
- sit9531x_dco_adjust() and sit9531x_pll_sysref_mode_set() have no
caller in this patch. They are infrastructure for follow-up work
(the DCO path pairs with a loop-bandwidth netlink attribute; SYSREF
mode needs a new netlink op). Kept because the helpers are
non-trivial and removing them would require re-review when re-added.
- dco_calc_outer(): the DIVN2 integer part is read from a 35-bit
register; for low-frequency outer references the divn2_num product
can exceed u64 when fracd_eff defaults to 2^32. The arithmetic is
split so no intermediate overflows: divn2_num formation uses
mul_u64_u64_div_u64() where needed, and the downstream ppb products
already use 128-bit helpers.
- The reset GPIO is intentionally never pulsed. The chip configuration
is loaded from efuse or an NVM blob before probe, so a runtime reset
would discard it; the line is only held deasserted to guarantee the
chip is out of reset for I2C.
- Automatic vs manual mode: on this chip automatic means the PLL tracks
a reference (sync) and manual means free-run. There is no separate
mode to turn off automatic input selection while still syncing (as
zl3073x has); the closest analogue, sync with a manually pinned input
(MANUAL_ACTIVE_SEL_PLL + man_in_sel), is documented in the code but
not wired to a netlink op yet.
Tested on a SiT95316EBX board (I2C, 96 MHz XO): input enable/disable and
priority, input disconnect and reconnect, INTSYNC lock, output frequency
set (scope-verified), phase-adjust, esync pulse, and phase-offset
readback. checkpatch --strict is clean across the series.
Ali Rouhi (3):
dt-bindings: vendor-prefixes: add SiTime Corporation
dt-bindings: dpll: add SiTime SiT9531x clock generator
dpll: add SiTime SiT9531x DPLL clock driver
.../bindings/dpll/sitime,sit9531x.yaml | 201 +
.../devicetree/bindings/vendor-prefixes.yaml | 2 +
MAINTAINERS | 7 +
drivers/dpll/Kconfig | 1 +
drivers/dpll/Makefile | 1 +
drivers/dpll/sit9531x/Kconfig | 17 +
drivers/dpll/sit9531x/Makefile | 4 +
drivers/dpll/sit9531x/core.c | 3609 +++++++++++++++++
drivers/dpll/sit9531x/core.h | 388 ++
drivers/dpll/sit9531x/dpll.c | 1580 ++++++++
drivers/dpll/sit9531x/dpll.h | 87 +
drivers/dpll/sit9531x/prop.c | 415 ++
drivers/dpll/sit9531x/prop.h | 39 +
drivers/dpll/sit9531x/regs.h | 419 ++
14 files changed, 6770 insertions(+)
create mode 100644 Documentation/devicetree/bindings/dpll/sitime,sit9531x.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
--
2.43.0
This email message and any attachments are for the sole use of the intended recipient and contain proprietary and/or confidential information that may be privileged or otherwise protected from disclosure. Any unauthorized review, use, disclosure, copying or distribution of the information included in this message and any attachment is prohibited and may be unlawful. If you are not an intended recipient, please contact the sender by reply email and permanently delete and destroy the original message and any attachments, and any printouts or copies of the message and attachments to the original message. Thank you.
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v3 net-next 2/3] dt-bindings: dpll: add SiTime SiT9531x clock generator
2026-07-31 18:09 [PATCH v3 net-next 0/3] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
2026-07-31 18:09 ` [PATCH v3 net-next 1/3] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
@ 2026-07-31 18:09 ` Ali Rouhi
2026-07-31 18:09 ` [PATCH v3 net-next 3/3] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
2 siblings, 0 replies; 4+ messages in thread
From: Ali Rouhi @ 2026-07-31 18:09 UTC (permalink / raw)
To: jiri@resnulli.us
Cc: vadim.fedorenko@linux.dev, arkadiusz.kubalewski@intel.com,
robh@kernel.org, krzk+dt@kernel.org, conor+dt@kernel.org,
cjubran@nvidia.com, Oleg.Zadorozhnyi@devoxsoftware.com,
devicetree@vger.kernel.org, netdev@vger.kernel.org,
linux-kernel@vger.kernel.org, Ali Rouhi
Add device tree binding documentation for the SiTime SiT95316
and SiT95317 DPLL clock generators.
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
.../bindings/dpll/sitime,sit9531x.yaml | 203 ++++++++++++++++++
1 file changed, 203 insertions(+)
create mode 100644 Documentation/devicetree/bindings/dpll/sitime,sit9531x.yaml
diff --git a/Documentation/devicetree/bindings/dpll/sitime,sit9531x.yaml b/Documentation/devicetree/bindings/dpll/sitime,sit9531x.yaml
new file mode 100644
index 000000000000..56a4f5135fb9
--- /dev/null
+++ b/Documentation/devicetree/bindings/dpll/sitime,sit9531x.yaml
@@ -0,0 +1,203 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/dpll/sitime,sit9531x.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: SiTime SiT9531x 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).
+
+ SiT95317 provides 4 inputs and 8 outputs; SiT95316 provides
+ 4 inputs and 12 outputs.
+
+properties:
+ compatible:
+ enum:
+ - sitime,sit95316
+ - sitime,sit95317
+
+ reg:
+ maxItems: 1
+
+ clocks:
+ maxItems: 1
+
+ clock-names:
+ items:
+ - const: xtal
+
+ clock-frequency:
+ description:
+ XO/xtal input frequency in Hz. Fallback for firmware that does not
+ expose the oscillator through the clock framework; prefer the
+ "clocks" phandle when the clock is available.
+
+ reset-gpios:
+ maxItems: 1
+ description:
+ GPIO connected to the chip's active-low reset pin (RESETB).
+
+ interrupts:
+ maxItems: 1
+ description:
+ Interrupt from the chip's active-low INTRB output. Asserted when
+ the device detects a status change such as lock acquisition or loss.
+
+ sitime,pll-fvco:
+ $ref: /schemas/types.yaml#/definitions/uint64-array
+ minItems: 4
+ maxItems: 4
+ description:
+ Per-PLL VCO frequency in Hz for PLLA, PLLB, PLLC, PLLD. The values
+ exceed 32 bits, so this uses a uint64-array and does not carry the
+ "-hz" unit suffix (which implies a 32-bit cell). Override
+ for configurations where the standard Fvco = Fref * DIVN derivation
+ does not match the running VCO -- for example a PLL operating in
+ INTSYNC mode, or a chip variant whose VCO falls outside the
+ documented PLL_FVCO band. A value of 0 keeps the register-derived
+ computation for that PLL.
+
+ sitime,output-pll-map:
+ $ref: /schemas/types.yaml#/definitions/uint32-array
+ minItems: 8
+ maxItems: 12
+ items:
+ enum: [0, 1, 2, 3, 255]
+ description:
+ Source PLL index (0=PLLA .. 3=PLLD) for each output 0..11. The
+ value 255 (0xff) marks an output as unmapped and prevents the
+ driver from registering it as a DPLL pin. Override for
+ configurations where the chip's per-PLL OUTPUT_ENABLE bitmaps do
+ not unambiguously describe output-to-PLL routing.
+
+required:
+ - compatible
+ - reg
+
+oneOf:
+ - required:
+ - clocks
+ - clock-names
+ - required:
+ - clock-frequency
+
+allOf:
+ - $ref: /schemas/dpll/dpll-device.yaml#
+ # SiT95317 exposes 8 outputs, SiT95316 exposes 12. Bound the
+ # output-pll-map length to the variant so a SiT95317 node cannot
+ # describe more outputs than the part has.
+ - if:
+ properties:
+ compatible:
+ contains:
+ const: sitime,sit95317
+ then:
+ properties:
+ sitime,output-pll-map:
+ maxItems: 8
+ # SiT95316 has 12 outputs. When the map is supplied it must describe
+ # all of them, otherwise the trailing outputs are left ambiguous.
+ - if:
+ properties:
+ compatible:
+ contains:
+ const: sitime,sit95316
+ then:
+ properties:
+ sitime,output-pll-map:
+ minItems: 12
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ dpll@68 {
+ compatible = "sitime,sit95317";
+ reg = <0x68>;
+ clocks = <&xo>;
+ clock-names = "xtal";
+ };
+ };
+
+ - |
+ /* XO rate from a property when the clock framework cannot provide it */
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ dpll@6d {
+ compatible = "sitime,sit95316";
+ reg = <0x6d>;
+ clock-frequency = <96000000>;
+ };
+ };
+
+ - |
+ #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";
+
+ input-pins {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ pin@0 {
+ reg = <0>;
+ label = "clkin0";
+ connection-type = "ext";
+ supported-frequencies-hz = /bits/ 64 <10000000>;
+ };
+
+ pin@1 {
+ reg = <1>;
+ label = "clkin1";
+ connection-type = "synce";
+ supported-frequencies-hz = /bits/ 64 <156250000>;
+ };
+ };
+
+ output-pins {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ pin@0 {
+ reg = <0>;
+ label = "clkout0";
+ esync-control;
+ supported-frequencies-hz = /bits/ 64 <156250000>;
+ };
+
+ pin@1 {
+ reg = <1>;
+ label = "clkout1";
+ supported-frequencies-hz = /bits/ 64 <25000000>;
+ };
+ };
+ };
+ };
+...
--
2.43.0
This email message and any attachments are for the sole use of the intended recipient and contain proprietary and/or confidential information that may be privileged or otherwise protected from disclosure. Any unauthorized review, use, disclosure, copying or distribution of the information included in this message and any attachment is prohibited and may be unlawful. If you are not an intended recipient, please contact the sender by reply email and permanently delete and destroy the original message and any attachments, and any printouts or copies of the message and attachments to the original message. Thank you.
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH v3 net-next 3/3] dpll: add SiTime SiT9531x DPLL clock driver
2026-07-31 18:09 [PATCH v3 net-next 0/3] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
2026-07-31 18:09 ` [PATCH v3 net-next 1/3] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
2026-07-31 18:09 ` [PATCH v3 net-next 2/3] dt-bindings: dpll: add SiTime SiT9531x clock generator Ali Rouhi
@ 2026-07-31 18:09 ` Ali Rouhi
2 siblings, 0 replies; 4+ messages in thread
From: Ali Rouhi @ 2026-07-31 18:09 UTC (permalink / raw)
To: jiri@resnulli.us
Cc: vadim.fedorenko@linux.dev, arkadiusz.kubalewski@intel.com,
robh@kernel.org, krzk+dt@kernel.org, conor+dt@kernel.org,
cjubran@nvidia.com, Oleg.Zadorozhnyi@devoxsoftware.com,
devicetree@vger.kernel.org, netdev@vger.kernel.org,
linux-kernel@vger.kernel.org, Ali Rouhi
Add a DPLL subsystem driver for the SiTime SiT95316 and SiT95317
clock generators. These devices provide low-jitter clock outputs
commonly used in telecom, networking, and data center timing
applications.
The driver exposes all inputs and outputs through the Linux DPLL
subsystem, supporting:
- Lock status monitoring via register polling or optional INTRB IRQ
- Input priority management for automatic reference switchover
- Per-output frequency readback from hardware state
- DCO (digitally controlled oscillator) frequency adjustment
- Phase offset measurement via TDC (time-to-digital converter)
- Phase adjustment for fine output alignment
- Embedded sync (esync) pulse control
- SYSREF/SYNCB/Pulser output mode control
- Optional reset-gpios for hardware reset
The driver reads all configuration from the device's on-chip NVM
at probe time -- no firmware loading is required.
Co-developed-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
MAINTAINERS | 7 +
drivers/dpll/Kconfig | 1 +
drivers/dpll/Makefile | 1 +
drivers/dpll/sit9531x/Kconfig | 17 +
drivers/dpll/sit9531x/Makefile | 4 +
drivers/dpll/sit9531x/core.c | 3609 ++++++++++++++++++++++++++++++++
drivers/dpll/sit9531x/core.h | 388 ++++
drivers/dpll/sit9531x/dpll.c | 1580 ++++++++++++++
drivers/dpll/sit9531x/dpll.h | 87 +
drivers/dpll/sit9531x/prop.c | 415 ++++
drivers/dpll/sit9531x/prop.h | 39 +
drivers/dpll/sit9531x/regs.h | 419 ++++
12 files changed, 6567 insertions(+)
create mode 100644 drivers/dpll/sit9531x/Kconfig
create mode 100644 drivers/dpll/sit9531x/Makefile
create mode 100644 drivers/dpll/sit9531x/core.c
create mode 100644 drivers/dpll/sit9531x/core.h
create mode 100644 drivers/dpll/sit9531x/dpll.c
create mode 100644 drivers/dpll/sit9531x/dpll.h
create mode 100644 drivers/dpll/sit9531x/prop.c
create mode 100644 drivers/dpll/sit9531x/prop.h
create mode 100644 drivers/dpll/sit9531x/regs.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 932ea1db048e..eff783481b2b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -24912,6 +24912,13 @@ S: Maintained
W: http://www.winischhofer.at/linuxsisusbvga.shtml
F: drivers/usb/misc/sisusbvga/
+SITIME SIT9531X DPLL DRIVER
+M: Ali Rouhi <arouhi@sitime.com>
+L: netdev@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/dpll/sitime,sit9531x.yaml
+F: drivers/dpll/sit9531x/
+
SL28 CPLD MFD DRIVER
M: Michael Walle <mwalle@kernel.org>
S: Maintained
diff --git a/drivers/dpll/Kconfig b/drivers/dpll/Kconfig
index be98969f040a..ea77b9c11ab1 100644
--- a/drivers/dpll/Kconfig
+++ b/drivers/dpll/Kconfig
@@ -23,6 +23,7 @@ config DPLL_REFCNT_TRACKER
If unsure, say N.
+source "drivers/dpll/sit9531x/Kconfig"
source "drivers/dpll/zl3073x/Kconfig"
endmenu
diff --git a/drivers/dpll/Makefile b/drivers/dpll/Makefile
index 9e7a3a3e592e..4adc50d748d4 100644
--- a/drivers/dpll/Makefile
+++ b/drivers/dpll/Makefile
@@ -8,4 +8,5 @@ dpll-y += dpll_core.o
dpll-y += dpll_netlink.o
dpll-y += dpll_nl.o
+obj-$(CONFIG_SIT9531X_DPLL) += sit9531x/
obj-$(CONFIG_ZL3073X) += zl3073x/
diff --git a/drivers/dpll/sit9531x/Kconfig b/drivers/dpll/sit9531x/Kconfig
new file mode 100644
index 000000000000..47aea8674327
--- /dev/null
+++ b/drivers/dpll/sit9531x/Kconfig
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+config SIT9531X_DPLL
+ tristate "SiTime SiT9531x DPLL driver"
+ depends on I2C && NET
+ select DPLL
+ select REGMAP_I2C
+ help
+ Driver for SiTime SiT9531x family clock generators
+ (SiT95317, SiT95316).
+
+ This driver registers each on-chip PLL as a DPLL device
+ and exposes input/output clocks as DPLL pins, providing
+ runtime configuration via Generic Netlink.
+
+ To compile this driver as a module, choose M here: the
+ module will be called sit9531x.
diff --git a/drivers/dpll/sit9531x/Makefile b/drivers/dpll/sit9531x/Makefile
new file mode 100644
index 000000000000..b97d2656a460
--- /dev/null
+++ b/drivers/dpll/sit9531x/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+obj-$(CONFIG_SIT9531X_DPLL) += sit9531x.o
+sit9531x-y := core.o dpll.o prop.o
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
new file mode 100644
index 000000000000..fecea4201e4d
--- /dev/null
+++ b/drivers/dpll/sit9531x/core.c
@@ -0,0 +1,3609 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * SiTime SiT9531x DPLL core driver
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * Core I2C probe, regmap configuration, hardware state management,
+ * and periodic work thread.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/device.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/kernel.h>
+#include <linux/kthread.h>
+#include <linux/module.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#include "core.h"
+#include "dpll.h"
+#include "regs.h"
+
+/* ====================================================================
+ * Chip variant table
+ * ====================================================================
+ */
+
+#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),
+};
+
+/* ====================================================================
+ * Regmap configuration
+ * ====================================================================
+ */
+
+#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,
+};
+
+/* ====================================================================
+ * Register access helpers
+ * ====================================================================
+ */
+
+/**
+ * sit9531x_read_u8 - read an 8-bit register
+ * @sitdev: device pointer
+ * @reg: register in SIT9531X_REG(page, offset) form
+ * @val: output value
+ *
+ * Return: 0 on success, <0 on error
+ */
+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
+ * @sitdev: device pointer
+ * @reg: register in SIT9531X_REG(page, offset) form
+ * @val: value to write
+ *
+ * Return: 0 on success, <0 on error
+ */
+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
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ * @offset: register offset within the PLL page
+ * @val: output value
+ *
+ * Return: 0 on success, <0 on error
+ */
+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
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ * @offset: register offset within the PLL page
+ * @val: value to write
+ *
+ * Return: 0 on success, <0 on error
+ */
+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
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ * @offset: register offset within the PLL page
+ * @mask: bits to modify
+ * @val: new value for masked bits
+ *
+ * Return: 0 on success, <0 on error
+ */
+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);
+}
+
+/* ====================================================================
+ * Input enable / disable
+ * ====================================================================
+ */
+
+/*
+ * sit9531x_input_get_regs - get force mask and state register addresses
+ * @sitdev: device pointer
+ * @index: logical input index
+ * @force_reg: output force mask register address
+ * @state_reg: output state register address
+ *
+ * Selects the correct Page 0x02 register pair based on the pair's
+ * signal mode and the lane (P/N) the index refers to.
+ */
+static void sit9531x_input_get_regs(const struct sit9531x_dev *sitdev,
+ u8 index,
+ unsigned int *force_reg,
+ unsigned int *state_reg)
+{
+ if (sitdev->ref[index].sig_mode == SIT9531X_MODE_DE) {
+ *force_reg = SIT9531X_REG_IN_DE_FORCE;
+ *state_reg = SIT9531X_REG_IN_DE_STATE;
+ } else if (sit9531x_input_is_n(index)) {
+ *force_reg = SIT9531X_REG_IN_SEN_FORCE;
+ *state_reg = SIT9531X_REG_IN_SEN_STATE;
+ } else {
+ *force_reg = SIT9531X_REG_IN_SEP_FORCE;
+ *state_reg = SIT9531X_REG_IN_SEP_STATE;
+ }
+}
+
+/**
+ * sit9531x_input_disable - disable an input reference
+ * @sitdev: device pointer
+ * @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.
+ *
+ * Return: 0 on success, <0 on error
+ */
+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);
+
+ /* Set force mask bit: enable override */
+ 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;
+
+ /* Clear state bit: 0 = disabled */
+ 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
+ * @sitdev: device pointer
+ * @index: logical input index (0-N)
+ *
+ * Clears the force mask bit for the given input, returning it to
+ * hardware default (enabled).
+ *
+ * Return: 0 on success, <0 on error
+ */
+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);
+
+ /* Clear force mask bit: return to hardware default (enabled) */
+ rc = sit9531x_read_u8(sitdev, force_reg, &val);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, force_reg, val & ~BIT(pair));
+ if (rc)
+ return rc;
+
+ ref->enabled = true;
+
+ return 0;
+}
+
+/* ====================================================================
+ * Output enable / disable (Hi-Z control)
+ *
+ * SiT9531x outputs can be configured as differential (DIFF) or
+ * single-ended (SE) depending on the factory blob. Each output slot
+ * has TWO Hi-Z force/state register pairs on Page 0x03 -- one for the
+ * DIFF path, one for the SE path.
+ *
+ * We write to BOTH pairs so the function mutes the output regardless
+ * of whether it's been configured DIFF or SE on this board.
+ *
+ * slot 0-7 :
+ * DIFF mask=0xF2 state=0xF3 SE mask=0xF8 state=0xF9
+ * slot 8-11:
+ * DIFF mask=0xF4 state=0xF5 SE mask=0xFA state=0xFB
+ *
+ * MASK bit = 1 -> driver takes control of that output's Hi-Z state
+ * STATE bit = 0 -> output is forced to Hi-Z (muted)
+ * STATE bit = 1 -> output is driven (active)
+ *
+ * The output "index" in the driver is logical; the physical slot comes
+ * from info->clkout_map[].
+ * ====================================================================
+ */
+
+struct sit9531x_hiz_regs {
+ unsigned int diff_mask;
+ unsigned int diff_state;
+ unsigned int se_mask;
+ unsigned int se_state;
+ u8 bit;
+};
+
+static void sit9531x_output_get_hiz_regs(u8 slot,
+ struct sit9531x_hiz_regs *r)
+{
+ if (slot <= 7) {
+ r->diff_mask = SIT9531X_REG_HIZ_DIFF_07_MASK;
+ r->diff_state = SIT9531X_REG_HIZ_DIFF_07_STATE;
+ r->se_mask = SIT9531X_REG_HIZ_SE_07_MASK;
+ r->se_state = SIT9531X_REG_HIZ_SE_07_STATE;
+ r->bit = slot;
+ } else {
+ r->diff_mask = SIT9531X_REG_HIZ_DIFF_811_MASK;
+ r->diff_state = SIT9531X_REG_HIZ_DIFF_811_STATE;
+ r->se_mask = SIT9531X_REG_HIZ_SE_811_MASK;
+ r->se_state = SIT9531X_REG_HIZ_SE_811_STATE;
+ r->bit = slot - 8;
+ }
+}
+
+/*
+ * Report whether a slot is currently forced into Hi-Z, i.e. the driver
+ * (or the blob) took control of the Hi-Z state (MASK bit set) and drives
+ * it low (STATE bit clear). Either register pair muting the slot counts,
+ * mirroring what sit9531x_output_disable() programs.
+ */
+static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev, u8 slot,
+ bool *muted)
+{
+ struct sit9531x_hiz_regs r;
+ u8 mask, state;
+ int rc;
+
+ sit9531x_output_get_hiz_regs(slot, &r);
+
+ rc = sit9531x_read_u8(sitdev, r.diff_mask, &mask);
+ if (rc)
+ return rc;
+ rc = sit9531x_read_u8(sitdev, r.diff_state, &state);
+ if (rc)
+ return rc;
+
+ *muted = (mask & BIT(r.bit)) && !(state & BIT(r.bit));
+ if (*muted)
+ return 0;
+
+ rc = sit9531x_read_u8(sitdev, r.se_mask, &mask);
+ if (rc)
+ return rc;
+ rc = sit9531x_read_u8(sitdev, r.se_state, &state);
+ if (rc)
+ return rc;
+
+ *muted = (mask & BIT(r.bit)) && !(state & BIT(r.bit));
+
+ return 0;
+}
+
+static int sit9531x_hiz_set_bit(struct sit9531x_dev *sitdev,
+ unsigned int reg, u8 bit, bool set)
+{
+ u8 cur, new_val;
+ int rc;
+
+ rc = sit9531x_read_u8(sitdev, reg, &cur);
+ if (rc)
+ return rc;
+
+ new_val = set ? (cur | BIT(bit)) : (cur & ~BIT(bit));
+
+ return sit9531x_write_u8(sitdev, reg, new_val);
+}
+
+/*
+ * Enter the output-system programming state: unlock the debug
+ * registers on Page 3 and issue the PRG_CMD state command. Register
+ * writes that reconfigure the output system only take effect when
+ * they are made inside this state.
+ */
+static int sit9531x_prg_enter(struct sit9531x_dev *sitdev)
+{
+ int rc;
+
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
+ SIT9531X_DEBUG_UNLOCK_VAL);
+ if (rc)
+ return rc;
+
+ return sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+ SIT9531X_PRG_CMD_STATE);
+}
+
+/*
+ * Commit a programming sequence started by sit9531x_prg_enter():
+ * update the NVM shadow and re-lock the loops. The sleep gives the
+ * hardware its required settling time after the loop-lock command;
+ * it is intentional despite the caller holding multiop_lock, as the
+ * whole NVM + lock sequence must be atomic.
+ */
+static int sit9531x_prg_commit(struct sit9531x_dev *sitdev)
+{
+ int rc;
+
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+ SIT9531X_UPDATE_NVM);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+ SIT9531X_LOOP_LOCK);
+ if (rc)
+ return rc;
+
+ msleep(100);
+
+ return 0;
+}
+
+/**
+ * sit9531x_output_disable - mute an output (force Hi-Z)
+ * @sitdev: device pointer
+ * @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.
+ *
+ * Return: 0 on success, <0 on error
+ */
+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)
+ * @sitdev: device pointer
+ * @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.
+ *
+ * Return: 0 on success, <0 on error
+ */
+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;
+
+ /* Release control (MASK=0) on both DIFF and SE */
+ rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, false);
+ if (rc)
+ goto commit;
+ rc = sit9531x_hiz_set_bit(sitdev, r.se_mask, r.bit, false);
+
+commit:
+ /*
+ * Always leave the PRG_CMD programming state, even on a mid-sequence
+ * write failure: prg_enter() unlocked the output loops, so returning
+ * without prg_commit() would strand the chip in the programming state
+ * with the loops unlocked. Best effort -- keep the first error.
+ */
+ ret = sit9531x_prg_commit(sitdev);
+ if (ret && !rc)
+ rc = ret;
+ if (!rc)
+ sitdev->out[index].enabled = true;
+
+ return rc;
+}
+
+/* ====================================================================
+ * Input priority selection
+ *
+ * The SiT9531x has a 12-slot priority table per PLL on Page 1. Each
+ * register holds two slots nibble-packed (even slot in [3:0], odd slot
+ * in [7:4]).
+ *
+ * The procedure:
+ * 1. Force PLL into holdover (PLL page reg 0x6F bit 4)
+ * 2. Enter PRG_CMD state (Page 0 reg 0x0F <- 0x01)
+ * 3. Write priority slots on Page 1
+ * 4. NVM update + loop lock (Page 0 reg 0x0F)
+ * 5. Release holdover
+ *
+ * Caller must hold sitdev->multiop_lock.
+ * ====================================================================
+ */
+
+/*
+ * Commit a priority-table programming sequence: update the NVM shadow
+ * and re-lock the loops through the Page-0 global update register.
+ * This is the priority-table counterpart of sit9531x_prg_commit(),
+ * which drives the output system through its own Page-3 register.
+ */
+static int sit9531x_prio_prg_commit(struct sit9531x_dev *sitdev)
+{
+ int rc;
+
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+ SIT9531X_UPDATE_NVM);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+ SIT9531X_LOOP_LOCK);
+ if (rc)
+ return rc;
+
+ usleep_range(1000, 2000);
+
+ return 0;
+}
+
+/**
+ * sit9531x_input_prio_set - set input priority for a PLL
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ * @input_idx: input source in hardware encoding (0-11, see
+ * sit9531x_input_hw_src())
+ * @prio: priority slot position (0 = highest)
+ *
+ * Writes a single priority slot assignment. The input source is
+ * placed at the requested slot, and all lower-priority (higher-numbered)
+ * slots are filled with the same source to avoid stale entries.
+ *
+ * Return: 0 on success, <0 on error
+ */
+int sit9531x_input_prio_set(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 input_idx, u8 prio)
+{
+ u8 pll_offset, reg_addr, val, slot;
+ int rc, prg_rc, ho_rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+ if (input_idx >= SIT9531X_PRIO_MAX_SLOTS)
+ return -EINVAL;
+ if (prio >= SIT9531X_PRIO_MAX_SLOTS)
+ return -EINVAL;
+
+ pll_offset = SIT9531X_PRIO_REGS_PER_PLL * pll_idx;
+
+ /* Step 1: Force 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));
+ if (rc)
+ return rc;
+
+ usleep_range(10000, 12000);
+
+ /* Step 2: Enter PRG_CMD state (Page 0, reg 0x0F) */
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+ SIT9531X_PRG_CMD_STATE);
+ if (rc)
+ goto release_ho;
+
+ /* Step 3: Write requested priority slot on Page 1 */
+ reg_addr = SIT9531X_PRIO_BASE_REG + pll_offset + (prio / 2);
+
+ rc = sit9531x_read_u8(sitdev,
+ SIT9531X_REG(SIT9531X_PAGE_PRIOSYS, reg_addr), &val);
+ if (rc)
+ goto commit;
+
+ if (prio & 1) {
+ /* Odd slot: bits [7:4] */
+ val = (val & SIT9531X_PRIO_NIBBLE_MASK) |
+ (input_idx << SIT9531X_PRIO_HI_SHIFT);
+ } else {
+ /* Even slot: bits [3:0] */
+ val = (val & (SIT9531X_PRIO_NIBBLE_MASK << SIT9531X_PRIO_HI_SHIFT)) |
+ (input_idx & SIT9531X_PRIO_NIBBLE_MASK);
+ }
+
+ rc = sit9531x_write_u8(sitdev,
+ SIT9531X_REG(SIT9531X_PAGE_PRIOSYS, reg_addr), val);
+ if (rc)
+ goto commit;
+
+ /*
+ * Fill remaining slots (prio+1 .. 11) with the same source,
+ * matching the procedure script's backfill behaviour.
+ */
+ for (slot = prio + 1; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+ reg_addr = SIT9531X_PRIO_BASE_REG + pll_offset + (slot / 2);
+
+ rc = sit9531x_read_u8(sitdev,
+ SIT9531X_REG(SIT9531X_PAGE_PRIOSYS, reg_addr),
+ &val);
+ if (rc)
+ goto commit;
+
+ if (slot & 1)
+ val = (val & SIT9531X_PRIO_NIBBLE_MASK) |
+ (input_idx << SIT9531X_PRIO_HI_SHIFT);
+ else
+ val = (val & (SIT9531X_PRIO_NIBBLE_MASK <<
+ SIT9531X_PRIO_HI_SHIFT)) |
+ (input_idx & SIT9531X_PRIO_NIBBLE_MASK);
+
+ rc = sit9531x_write_u8(sitdev,
+ SIT9531X_REG(SIT9531X_PAGE_PRIOSYS, reg_addr),
+ val);
+ if (rc)
+ goto commit;
+ }
+
+commit:
+ /*
+ * Steps 4-5: NVM update and loop lock. These must run even when a
+ * slot write failed: entering the PRG_CMD state above unlocked the
+ * loops, so returning without the commit would strand the chip in
+ * the programming state. Best effort -- keep the first error.
+ */
+ prg_rc = sit9531x_prio_prg_commit(sitdev);
+ if (prg_rc && !rc)
+ rc = prg_rc;
+
+release_ho:
+ /*
+ * Step 6: Release holdover. This must run even when an earlier step
+ * failed, so its own error is only surfaced when it would not mask a
+ * prior failure -- otherwise a failing release could leave the PLL
+ * stuck in forced holdover while the function reports success.
+ */
+ 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_input_prio_get - read an input's priority slot for a PLL
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ * @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.
+ *
+ * Return: 0 on success, <0 on error
+ */
+int sit9531x_input_prio_get(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 input_idx, u8 *prio)
+{
+ u8 pll_offset, reg_addr, val, slot, src;
+ int rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+
+ pll_offset = SIT9531X_PRIO_REGS_PER_PLL * pll_idx;
+
+ for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+ reg_addr = SIT9531X_PRIO_BASE_REG + pll_offset + (slot / 2);
+
+ rc = sit9531x_read_u8(sitdev,
+ SIT9531X_REG(SIT9531X_PAGE_PRIOSYS, reg_addr),
+ &val);
+ if (rc)
+ return rc;
+
+ if (slot & 1)
+ src = val >> SIT9531X_PRIO_HI_SHIFT;
+ else
+ src = val & SIT9531X_PRIO_NIBBLE_MASK;
+
+ if (src == input_idx) {
+ *prio = slot;
+ return 0;
+ }
+ }
+
+ *prio = SIT9531X_PRIO_MAX_SLOTS;
+ return 0;
+}
+
+/*
+ * sit9531x_prio_table_commit - write a full priority table for a PLL
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ * @srcs: array of SIT9531X_PRIO_MAX_SLOTS source codes, slot 0 first
+ *
+ * Programs all 12 priority slots (nibble-packed, two per register) for
+ * the PLL using the same holdover / PRG_CMD / NVM-update / loop-lock
+ * 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 pll_offset, reg_addr, val, slot;
+ int rc, prg_rc, ho_rc;
+
+ pll_offset = SIT9531X_PRIO_REGS_PER_PLL * pll_idx;
+
+ /* Step 1: force 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));
+ if (rc)
+ return rc;
+
+ usleep_range(10000, 12000);
+
+ /* Step 2: enter PRG_CMD state */
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+ SIT9531X_PRG_CMD_STATE);
+ if (rc)
+ goto release_ho;
+
+ /* Step 3: write all slots on Page 1 */
+ for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+ reg_addr = SIT9531X_PRIO_BASE_REG + pll_offset + (slot / 2);
+
+ rc = sit9531x_read_u8(sitdev,
+ SIT9531X_REG(SIT9531X_PAGE_PRIOSYS, reg_addr),
+ &val);
+ if (rc)
+ goto commit;
+
+ if (slot & 1)
+ val = (val & SIT9531X_PRIO_NIBBLE_MASK) |
+ (srcs[slot] << SIT9531X_PRIO_HI_SHIFT);
+ else
+ val = (val & (SIT9531X_PRIO_NIBBLE_MASK <<
+ SIT9531X_PRIO_HI_SHIFT)) |
+ (srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK);
+
+ rc = sit9531x_write_u8(sitdev,
+ SIT9531X_REG(SIT9531X_PAGE_PRIOSYS, reg_addr),
+ val);
+ if (rc)
+ goto commit;
+ }
+
+commit:
+ /*
+ * Steps 4-5: NVM update and loop lock, run unconditionally for the
+ * same reason as in sit9531x_input_prio_set() -- a mid-table write
+ * failure must not leave the chip in the PRG_CMD state with the
+ * loops unlocked.
+ */
+ prg_rc = sit9531x_prio_prg_commit(sitdev);
+ if (prg_rc && !rc)
+ rc = prg_rc;
+
+release_ho:
+ 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 12 priority-table source codes
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ * @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 pll_offset, reg_addr, val, slot;
+ int rc;
+
+ pll_offset = SIT9531X_PRIO_REGS_PER_PLL * pll_idx;
+
+ for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+ reg_addr = SIT9531X_PRIO_BASE_REG + pll_offset + (slot / 2);
+
+ rc = sit9531x_read_u8(sitdev,
+ SIT9531X_REG(SIT9531X_PAGE_PRIOSYS, reg_addr),
+ &val);
+ if (rc)
+ return rc;
+
+ if (slot & 1)
+ srcs[slot] = val >> SIT9531X_PRIO_HI_SHIFT;
+ else
+ srcs[slot] = val & SIT9531X_PRIO_NIBBLE_MASK;
+ }
+
+ return 0;
+}
+
+/**
+ * sit9531x_input_prio_remove - drop an input from a PLL's priority table
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ * @input_idx: input source in hardware encoding
+ *
+ * Rewrites the priority table with the source removed: the remaining
+ * sources are compacted toward the highest-priority slots and the freed
+ * tail slots are backfilled with the lowest-priority remaining source
+ * (matching sit9531x_input_prio_set()). This makes a disconnected
+ * input ineligible for automatic reference selection, not just gated at
+ * the input buffer. It is a no-op when the source is absent, or when it
+ * is the only source (the buffer disable then drives the PLL to
+ * holdover and the table must not be left empty).
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, <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 || count == 0)
+ return 0;
+
+ /* Backfill freed tail slots with the lowest-priority remaining src */
+ while (count < SIT9531X_PRIO_MAX_SLOTS) {
+ kept[count] = kept[count - 1];
+ count++;
+ }
+
+ return sit9531x_prio_table_commit(sitdev, pll_idx, kept);
+}
+
+/**
+ * sit9531x_input_prio_add - make an input eligible in a PLL's table
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ * @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.
+ *
+ * Return: 0 on success, <0 on error
+ */
+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);
+}
+
+/* ====================================================================
+ * DCO tuning (inner and outer loop)
+ *
+ * The SiT9531x DCO mechanism adjusts PLL frequency in ppb via
+ * fractional divider manipulation.
+ *
+ * Inner loop (free-run mode):
+ * - Reads DIVN integer/fraction from PLL page 0x30-0x3B
+ * - Computes 48-bit DCO fractional word
+ * - Writes to PLL page regs 0x51-0x56
+ *
+ * Outer loop (sync mode):
+ * - Reads DIVN2 integer/fraction from PLL page 0x3E-0x4C
+ * - Computes 24-bit integer + 32-bit fractional DCO word
+ * - Writes to PLL page regs 0x5E-0x60 (int), 0x63-0x66 (frac)
+ *
+ * After writing DCO codes, a trigger pulse on Page 0 reg 0x64
+ * applies the increment (bit 6) or decrement (bit 4).
+ * ====================================================================
+ */
+
+/**
+ * struct sit9531x_dco_code - computed DCO register values
+ * @dco_frac: 48-bit inner loop fractional word
+ * @dco_int: 24-bit outer loop integer word (signed)
+ * @outer_frac: 32-bit outer loop fractional word
+ */
+struct sit9531x_dco_code {
+ u64 dco_frac;
+ s32 dco_int;
+ u64 outer_frac;
+};
+
+/*
+ * sit9531x_dco_read_divn - read DIVN values for free-run DCO calculation
+ * @sitdev: device pointer
+ * @pll_idx: PLL index
+ * @int_part: output integer part
+ * @fracn: output fractional numerator
+ * @fracd: output fractional denominator
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int sit9531x_dco_read_divn(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u32 *int_part, u32 *fracn, u32 *fracd)
+{
+ u8 v, pbyq;
+ int rc, i;
+
+ /* Integer part */
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_DIVN_INT, &v);
+ if (rc)
+ return rc;
+ *int_part = v;
+
+ /* Numerator (4 bytes, little-endian) */
+ *fracn = 0;
+ 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 = (*fracn << 8) | v;
+ }
+
+ /* Check P/Q enable bit (reg 0x31 bit 6) */
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_STATUS, &v);
+ if (rc)
+ return rc;
+ pbyq = (v >> 6) & 1;
+
+ if (pbyq) {
+ /* Denominator (4 bytes, little-endian) */
+ *fracd = 0;
+ 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 = (*fracd << 8) | v;
+ }
+ } else {
+ /*
+ * P/Q disabled: integer-only mode.
+ * Set fracd = 1 to avoid division by zero.
+ */
+ *fracn = 0;
+ *fracd = 1;
+ }
+
+ return 0;
+}
+
+/*
+ * sit9531x_dco_read_divn2 - read DIVN2 values for sync DCO calculation
+ * @sitdev: device pointer
+ * @pll_idx: PLL index
+ * @n2_int: output integer part
+ * @n2_fracn: output fractional numerator
+ * @n2_fracd: output fractional denominator
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int sit9531x_dco_read_divn2(struct sit9531x_dev *sitdev, u8 pll_idx,
+ s64 *n2_int, u32 *n2_fracn, u32 *n2_fracd)
+{
+ int rc, i;
+ u8 v;
+
+ /* Integer part (5 bytes, big-endian: 0x42 is MSB) */
+ *n2_int = 0;
+ for (i = 4; i >= 0; i--) {
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DIVN2_INT + i, &v);
+ if (rc)
+ return rc;
+ if (i == 4)
+ v &= 0x07; /* only 3 bits in MSB */
+ *n2_int = (*n2_int << 8) | v;
+ }
+
+ /* Fractional numerator (4 bytes, big-endian: 0x46 is MSB) */
+ *n2_fracn = 0;
+ for (i = 3; i >= 0; i--) {
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DIVN2_FRAC_NUM + i,
+ &v);
+ if (rc)
+ return rc;
+ *n2_fracn = (*n2_fracn << 8) | v;
+ }
+
+ /* Fractional denominator (4 bytes, big-endian: 0x4C is MSB) */
+ *n2_fracd = 0;
+ for (i = 3; i >= 0; i--) {
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DIVN2_FRAC_DEN + i,
+ &v);
+ if (rc)
+ return rc;
+ *n2_fracd = (*n2_fracd << 8) | v;
+ }
+
+ return 0;
+}
+
+/*
+ * sit9531x_dco_calc_inner - compute inner loop DCO code (free-run)
+ *
+ * Formula:
+ * divn_code = int_part * 65536 + fracn * 65536 / fracd
+ * dco_frac = divn_code * ppb * fracd / 1e9
+ *
+ * Caller guarantees ppb >= 0; sign handled by trigger pulse.
+ */
+static void sit9531x_dco_calc_inner(u32 int_part, u32 fracn, u32 fracd,
+ s64 ppb, struct sit9531x_dco_code *code)
+{
+ u64 fracd_eff, frac_scaled, divn_code;
+
+ /*
+ * A zero denominator is the chip's implicit-denominator mode: fracn
+ * is a binary fraction over 2^32, not over 1 (matching
+ * sit9531x_get_fvco()). Defaulting to 1 would scale frac_scaled up
+ * by 2^32 and overflow it. Keep it in a u64 so the division cannot
+ * truncate.
+ */
+ fracd_eff = fracd ? fracd : (1ULL << 32);
+
+ frac_scaled = div64_u64((u64)fracn * 65536, fracd_eff);
+ divn_code = (u64)int_part * 65536 + frac_scaled;
+
+ /*
+ * The triple multiply (divn_code * ppb * fracd_eff) overflows u64 in
+ * realistic ranges (divn_code ~ 2^21, ppb ~ 2^30, fracd_eff ~ 2^32 =
+ * ~2^83). Use mul_u64_u64_div_u64() to carry out the multiply in
+ * 128 bits internally.
+ */
+ code->dco_frac = mul_u64_u64_div_u64(divn_code,
+ (u64)ppb * fracd_eff,
+ 1000000000ULL);
+}
+
+/*
+ * sit9531x_dco_calc_outer - compute outer loop DCO code (sync)
+ *
+ * The previous formulation computed divn2 with truncating integer
+ * division (n2_int + n2_fracn / n2_fracd), silently dropping the
+ * fractional part of DIVN2. Mirror the inner-loop scale-then-divide
+ * pattern by keeping the un-divided numerator:
+ * divn2_num = n2_int * n2_fracd + n2_fracn (exact)
+ * divn2 = divn2_num / n2_fracd (conceptually)
+ * dco_int = divn2 * ppb / 1e9
+ * = divn2_num * ppb / (n2_fracd * 1e9)
+ * outer_frac = (divn2 * ppb / 1e9 - dco_int) * n2_fracd * 65536
+ * = divn2_num * (ppb * 65536) / 1e9 - dco_int * n2_fracd * 65536
+ *
+ * Caller guarantees ppb >= 0; sign handled by trigger pulse.
+ */
+static void sit9531x_dco_calc_outer(s64 n2_int, u32 n2_fracn, u32 n2_fracd,
+ s64 ppb, struct sit9531x_dco_code *code)
+{
+ u64 fracd_eff, denom, a, b, ra, qa, c, quot, rem;
+
+ /*
+ * Conceptually dco_int = floor(divn2 * ppb / 1e9) and outer_frac =
+ * floor(frac(divn2 * ppb / 1e9) * fracd_eff * 65536), where
+ * divn2 = n2_int + n2_fracn / fracd_eff. Forming the numerator
+ * divn2_num = n2_int * fracd_eff + n2_fracn directly overflows u64:
+ * n2_int is the DIVN2 feedback integer read from a 35-bit register,
+ * so for a low-frequency outer reference (e.g. a 1 PPS input) it can
+ * approach 2^35, and with fracd_eff up to 2^32 the product reaches
+ * ~2^67. (The earlier "well under 2^20" note held only for the
+ * inner loop's high-frequency XTAL reference, not the outer loop.)
+ *
+ * Avoid ever materialising a value wider than u64 by expanding
+ * divn2_num * ppb = a * fracd_eff + b with a = n2_int * ppb and
+ * b = n2_fracn * ppb, then reducing a modulo 1e9 first:
+ * a = qa * 1e9 + ra (ra < 1e9)
+ * NUM = qa * denom + (ra * fracd_eff + b), denom = fracd_eff * 1e9
+ * dco_int = qa + (ra * fracd_eff + b) / denom
+ * outer_frac = (NUM mod denom) * 65536 / 1e9
+ * Every intermediate stays under 2^63; the final 65536 scaling uses
+ * the 128-bit helper. Caller guarantees n2_int >= 0 and ppb >= 0.
+ */
+ fracd_eff = n2_fracd ? n2_fracd : (1ULL << 32);
+ denom = fracd_eff * 1000000000ULL;
+
+ a = (u64)n2_int * (u64)ppb;
+ b = (u64)n2_fracn * (u64)ppb;
+
+ qa = div64_u64(a, 1000000000ULL);
+ ra = a - qa * 1000000000ULL;
+ c = ra * fracd_eff + b;
+
+ quot = div64_u64(c, denom);
+ rem = c - quot * denom;
+
+ code->dco_int = (s32)(qa + quot);
+ code->outer_frac = mul_u64_u64_div_u64(rem, 65536ULL, 1000000000ULL);
+}
+
+/*
+ * sit9531x_dco_write_inner - write inner loop DCO registers
+ */
+static int sit9531x_dco_write_inner(struct sit9531x_dev *sitdev,
+ u8 pll_idx,
+ const struct sit9531x_dco_code *code)
+{
+ u64 frac = code->dco_frac;
+ int rc, i;
+
+ /* Enable DCO with dither mode */
+ rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DCO_FUNC,
+ SIT9531X_DCO_DITHER_MODE | SIT9531X_DCO_EN);
+ if (rc)
+ return rc;
+
+ /* Write 48-bit fractional word (LSB first: 0x51->0x56) */
+ for (i = 0; i < 6; i++) {
+ rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DCO_FRAC1 + i,
+ frac & 0xFF);
+ if (rc)
+ return rc;
+ frac >>= 8;
+ }
+
+ return 0;
+}
+
+/*
+ * sit9531x_dco_write_outer - write outer loop DCO registers
+ */
+static int sit9531x_dco_write_outer(struct sit9531x_dev *sitdev,
+ u8 pll_idx,
+ const struct sit9531x_dco_code *code)
+{
+ s32 dco_int;
+ u64 frac;
+ int rc;
+
+ /* Enable DCO with dither mode + outer loop */
+ rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DCO_FUNC,
+ SIT9531X_DCO_DITHER_MODE | SIT9531X_DCO_OUTER_EN |
+ SIT9531X_DCO_EN);
+ if (rc)
+ return rc;
+
+ /* Write 32-bit fractional (shifted >>16, LSB first: 0x63->0x66) */
+ frac = code->outer_frac >> 16;
+ rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DCO_OFRAC_7, frac & 0xFF);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DCO_OFRAC_15,
+ (frac >> 8) & 0xFF);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DCO_OFRAC_23,
+ (frac >> 16) & 0xFF);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DCO_OFRAC_31,
+ (frac >> 24) & 0xFF);
+ if (rc)
+ return rc;
+
+ /* Write 24-bit integer (LSB first: 0x5E->0x60) */
+ dco_int = code->dco_int;
+ rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DCO_INT_7, dco_int & 0xFF);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DCO_INT_15,
+ (dco_int >> 8) & 0xFF);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DCO_INT_23,
+ (dco_int >> 16) & 0xFF);
+ if (rc)
+ return rc;
+
+ return 0;
+}
+
+/*
+ * sit9531x_dco_mask - mask a PLL's DCO (prevent updates)
+ */
+static int sit9531x_dco_mask(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+ u8 val;
+ int rc;
+
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DCO_FUNC, &val);
+ if (rc)
+ return rc;
+
+ return sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DCO_FUNC,
+ val | SIT9531X_DCO_MASK);
+}
+
+/*
+ * sit9531x_dco_unmask - unmask a PLL's DCO (allow updates)
+ */
+static int sit9531x_dco_unmask(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+ u8 val;
+ int rc;
+
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DCO_FUNC, &val);
+ if (rc)
+ return rc;
+
+ return sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DCO_FUNC,
+ val & ~SIT9531X_DCO_MASK);
+}
+
+/*
+ * sit9531x_dco_trigger - trigger DCO increment or decrement
+ * @sitdev: device pointer
+ * @neg_adj: true for decrement, false for increment
+ *
+ * Pulses the appropriate bit in Page 0 reg 0x64 (1->0 edge).
+ */
+static int sit9531x_dco_trigger(struct sit9531x_dev *sitdev, bool neg_adj)
+{
+ u8 val, bit;
+ int rc;
+
+ bit = neg_adj ? SIT9531X_DCO_TRIGGER_DECR : SIT9531X_DCO_TRIGGER_INCR;
+
+ /* Set trigger bit */
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_DCO_TRIGGER, &val);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_DCO_TRIGGER, val | bit);
+ if (rc)
+ return rc;
+
+ ndelay(SIT9531X_DCO_TRIGGER_PULSE_NS);
+
+ /* Clear trigger bit */
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_DCO_TRIGGER, &val);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_DCO_TRIGGER, val & ~bit);
+ if (rc)
+ return rc;
+
+ return 0;
+}
+
+/*
+ * Largest DCO pull accepted, in ppb (1e7 ppb = 1%,
+ * far past any physical DCO range). ppb is the only unbounded factor in
+ * the outer-loop products (divn2_num * ppb and dco_int * fracd_eff *
+ * 65536); the DIVN2 feedback divider read from the chip is a small
+ * physical value, so bounding ppb keeps those products inside 64 bits and
+ * rejects a bogus value instead of letting it wrap silently.
+ */
+#define SIT9531X_DCO_PPB_MAX 10000000LL
+
+/**
+ * sit9531x_dco_adjust - adjust PLL frequency via DCO
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ * @ppb: frequency adjustment in parts per billion
+ *
+ * In free-run mode, adjusts the inner loop only.
+ * In sync mode, adjusts both inner and outer loops.
+ * Masks all other PLLs' DCOs during the operation.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, -ERANGE if @ppb is out of range, <0 on error
+ */
+int sit9531x_dco_adjust(struct sit9531x_dev *sitdev, u8 pll_idx, s64 ppb)
+{
+ struct sit9531x_dco_code code = {};
+ bool neg_adj = false;
+ int rc, i;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ /*
+ * Bound before the negation below so S64_MIN cannot overflow, and so
+ * the fractional multiplies downstream stay within 64 bits.
+ */
+ if (ppb < -SIT9531X_DCO_PPB_MAX || ppb > SIT9531X_DCO_PPB_MAX)
+ return -ERANGE;
+
+ if (ppb < 0) {
+ neg_adj = true;
+ ppb = -ppb;
+ }
+
+ /*
+ * Mask all other PLLs' DCOs so the global DCO trigger (Page 0 reg
+ * 0x64) only steers @pll_idx. They are restored on every exit via
+ * the unmask_others label -- see the note there.
+ */
+ for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+ if (i == pll_idx)
+ continue;
+ rc = sit9531x_dco_mask(sitdev, i);
+ if (rc)
+ goto unmask_others;
+ }
+
+ /* Unmask target PLL */
+ rc = sit9531x_dco_unmask(sitdev, pll_idx);
+ if (rc)
+ goto unmask_others;
+
+ /* Unlock debug registers */
+ rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DEBUG, SIT9531X_PLL_DEBUG_UNLOCK);
+ if (rc)
+ goto unmask_others;
+
+ /* Determine mode and compute/write DCO codes */
+ if (sitdev->chan[pll_idx].mode) {
+ /* Free-run: inner loop only */
+ u32 div_int;
+ u32 div_fracn;
+ u32 div_fracd;
+
+ rc = sit9531x_dco_read_divn(sitdev, pll_idx,
+ &div_int, &div_fracn, &div_fracd);
+ if (rc)
+ goto unmask_others;
+
+ sit9531x_dco_calc_inner(div_int, div_fracn, div_fracd,
+ ppb, &code);
+
+ rc = sit9531x_dco_write_inner(sitdev, pll_idx, &code);
+ if (rc)
+ goto unmask_others;
+ } else {
+ /* Sync: inner + outer loops */
+ s64 n2_int;
+ u32 n2_fracn;
+ u32 n2_fracd;
+ u32 div_int;
+ u32 div_fracn;
+ u32 div_fracd;
+
+ /* Inner loop */
+ rc = sit9531x_dco_read_divn(sitdev, pll_idx,
+ &div_int, &div_fracn, &div_fracd);
+ if (rc)
+ goto unmask_others;
+
+ sit9531x_dco_calc_inner(div_int, div_fracn, div_fracd,
+ ppb, &code);
+
+ rc = sit9531x_dco_write_inner(sitdev, pll_idx, &code);
+ if (rc)
+ goto unmask_others;
+
+ /* Outer loop */
+ rc = sit9531x_dco_read_divn2(sitdev, pll_idx,
+ &n2_int, &n2_fracn, &n2_fracd);
+ if (rc)
+ goto unmask_others;
+
+ sit9531x_dco_calc_outer(n2_int, n2_fracn, n2_fracd,
+ ppb, &code);
+
+ rc = sit9531x_dco_write_outer(sitdev, pll_idx, &code);
+ if (rc)
+ goto unmask_others;
+ }
+
+ /* Set trigger register base value, then pulse increment/decrement */
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_DCO_TRIGGER,
+ SIT9531X_DCO_TRIGGER_BASE);
+ if (rc)
+ goto unmask_others;
+
+ rc = sit9531x_dco_trigger(sitdev, neg_adj);
+
+unmask_others:
+ /*
+ * The other PLLs were masked only to isolate @pll_idx for this
+ * adjustment. Their DCO mask bit is otherwise always clear (this
+ * function is the only writer of it), so unmask them again on every
+ * exit -- leaving them masked would freeze the outer loop on all other
+ * PLLs and silently break their reference tracking. Best effort: the
+ * first real error is preserved.
+ */
+ for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+ int err;
+
+ if (i == pll_idx)
+ continue;
+ err = sit9531x_dco_unmask(sitdev, i);
+ if (err && !rc)
+ rc = err;
+ }
+
+ return rc;
+}
+
+/* ====================================================================
+ * Output frequency set
+ * ====================================================================
+ */
+
+/* Per-slot DIVO base register offsets (6 slots per page) */
+static const u8 clkout_odr_divn_base[] = {
+ 0x14, 0x24, 0x34, 0x44, 0x54, 0x64
+};
+
+/* XO doubler register */
+#define SIT9531X_REG_XO2_GENERIC SIT9531X_REG(0x00, 0x2D)
+#define SIT9531X_XO_DOUBLER_ENB_BIT 7 /* inverted: 0 = enabled */
+
+/* VCO frequency bands (Hz) */
+#define SIT9531X_FVCO_LOWBAND_MIN 4915200000ULL
+#define SIT9531X_FVCO_LOWBAND_MAX 5898240000ULL
+#define SIT9531X_FVCO_HIGHBAND_MIN 6875000000ULL
+#define SIT9531X_FVCO_HIGHBAND_MAX 7812500000ULL
+
+/*
+ * sit9531x_is_xo_doubler_enabled - check if Fref doubler is active
+ * @sitdev: device pointer
+ *
+ * Register 0x2D bit 7 is active-low: 0 = doubler enabled, 1 = disabled.
+ *
+ * Return: 1 if enabled, 0 if disabled, <0 on error
+ */
+static int sit9531x_is_xo_doubler_enabled(struct sit9531x_dev *sitdev)
+{
+ u8 val;
+ int rc;
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_XO2_GENERIC, &val);
+ if (rc)
+ return rc;
+
+ return (~val >> SIT9531X_XO_DOUBLER_ENB_BIT) & 1u;
+}
+
+/*
+ * sit9531x_get_fvco - read VCO frequency from chip's DIVN registers
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ *
+ * Fvco = Fref * DIVN, where DIVN = int_part + fracn/fracd is read from
+ * PLL page regs 0x30 (int), 0x32-0x35 (numerator), 0x38-0x3B
+ * (denominator), 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.
+ *
+ * The numerator and denominator are unsigned 32-bit values. When the
+ * denominator reads as zero (the chip's "implicit denominator" mode for
+ * pure-fractional DIVN), the convention is fracd = 2^32, so fracn is
+ * interpreted as a binary fraction over 2^32.
+ *
+ * Return: Fvco in Hz, or 0 on error
+ */
+static u64 sit9531x_get_fvco(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+ u64 fracd = 0, fref, divn_temp;
+ u32 int_part, fracn = 0;
+ int doubler, rc, i;
+ u8 v;
+
+ /*
+ * 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_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DIVN_INT, &v);
+ if (rc)
+ return 0;
+ 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 0;
+ fracn = (fracn << 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 0;
+ fracd = (fracd << 8) | v;
+ }
+
+ /*
+ * Implicit denominator: fracd=0 means fracn is a binary fraction
+ * over 2^32 (the chip uses this when no explicit denominator is
+ * programmed).
+ */
+ if (!fracd)
+ fracd = 1ULL << 32;
+
+ doubler = sit9531x_is_xo_doubler_enabled(sitdev);
+ if (doubler < 0)
+ return 0;
+
+ fref = (u64)sitdev->xtal_freq << doubler;
+
+ /*
+ * Fvco = fref * (int_part + fracn/fracd). Split the multiplication
+ * to avoid 64-bit overflow on the realistic fref/DIVN range.
+ */
+ divn_temp = fref * int_part + div64_u64(fref * fracn, fracd);
+
+ return divn_temp;
+}
+
+/*
+ * sit9531x_output_phase_flush - flush the output phase of a PLL
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ *
+ * 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.
+ *
+ * Return: 0 on success, <0 on error
+ */
+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_PLL_SMALL_UPDATE_CMD,
+ SIT9531X_PLL_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
+ * @sitdev: device pointer
+ * @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;
+ }
+
+ /* Compute output divider: DIVO = Fvco / freq */
+ 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];
+
+ /* Step 1: Switch to Page 3 and unlock debug */
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
+ SIT9531X_DEBUG_UNLOCK_VAL);
+ if (rc)
+ return rc;
+
+ /* Step 2: Enter PRG_CMD state */
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+ SIT9531X_PRG_CMD_STATE);
+ if (rc)
+ return rc;
+
+ /* Step 3: Prepare DIVO bytes (34-bit, LSB at base reg) */
+ 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] */
+
+ /* Read existing MSB register and preserve upper 6 bits */
+ rc = sit9531x_read_u8(sitdev,
+ SIT9531X_REG(page, base_reg - 4), &msb_old);
+ if (rc)
+ goto commit;
+ divo_bytes[4] |= msb_old & 0xFC;
+
+ /* Write 5 DIVO bytes (base, base-1, base-2, base-3, base-4) */
+ 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;
+
+ /* Update cached output frequency */
+ sitdev->out[out_idx].freq = (u32)div64_u64(fvco, divo);
+
+ return 0;
+}
+
+/**
+ * sit9531x_output_freq_get - read output clock frequency from hardware
+ * @sitdev: device pointer
+ * @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];
+
+ /* Read 5 DIVO bytes (MSB at base-4, bits [1:0] only) */
+ for (j = 4; j >= 0; j--) {
+ rc = sit9531x_read_u8(sitdev,
+ SIT9531X_REG(page, base_reg - j), &v);
+ if (rc)
+ return rc;
+ if (j == 4)
+ v &= 0x03;
+ divo = (divo << 8) | v;
+ }
+
+ if (!divo)
+ return -ENODEV;
+
+ *frequency = div64_u64(fvco, divo);
+ sitdev->out[out_idx].freq = (u32)*frequency;
+
+ return 0;
+}
+
+/* ====================================================================
+ * Phase adjust (PRG_RST_DELAY register-based)
+ * ====================================================================
+ *
+ * The chip exposes a per-output 34-bit coarse delay measured in VCO
+ * clock periods plus a 3-bit fine delay in fixed 30 ps steps. The
+ * five bytes PROG6..PROG2 hold the field across registers:
+ * base + 0 PROG6 [7:5] OPSTG_VCASC_BUMP (preserved via RMW)
+ * [4:2] PRG_RST_FINE_DELAY
+ * [1:0] PRG_RST_DELAY[33:32]
+ * base + 1 PROG5 PRG_RST_DELAY[31:24]
+ * base + 2 PROG4 PRG_RST_DELAY[23:16]
+ * base + 3 PROG3 PRG_RST_DELAY[15:8]
+ * base + 4 PROG2 PRG_RST_DELAY[7:0]
+ *
+ * Outputs 0-5 live on Page 3, outputs 6-11 on Page 4, with each
+ * output's block at base = 0x15 + 16 * (out_idx % 6).
+ *
+ * The chip only supports unsigned positive delay. A negative phase
+ * adjustment (advance) is wrapped to (T_out - |phase|) modulo one
+ * output period, which is identical for a periodic signal.
+ */
+
+int sit9531x_output_phase_adjust_set(struct sit9531x_dev *sitdev,
+ u8 out_idx, s32 phase_ps)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ u64 abs_ps, fvco, coarse, coarse_ps, rem_ps;
+ u8 page, base, prog6_val, fine = 0;
+ u8 pll_idx, slot;
+ u32 freq;
+ int rc, ret;
+
+ if (out_idx >= info->num_outputs)
+ return -EINVAL;
+
+ pll_idx = sitdev->out[out_idx].pll_idx;
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+
+ freq = sitdev->out[out_idx].freq;
+ if (!freq)
+ return -EINVAL;
+
+ fvco = sit9531x_get_fvco(sitdev, pll_idx);
+ if (!fvco)
+ return -EIO;
+
+ /*
+ * Convert to unsigned absolute delay. Negative phase (advance)
+ * is rendered as T_out - |phase|, modulo the output period.
+ */
+ if (phase_ps == 0) {
+ abs_ps = 0;
+ } else if (phase_ps > 0) {
+ abs_ps = (u64)phase_ps;
+ } else {
+ u64 t_out_ps = div64_u64(1000000000000ULL, freq);
+ u64 advance = (u64)(-(s64)phase_ps);
+
+ if (t_out_ps == 0)
+ return -EINVAL;
+ advance %= t_out_ps;
+ abs_ps = (advance == 0) ? 0 : (t_out_ps - advance);
+ }
+
+ /*
+ * coarse_cycles = abs_ps * Fvco / 1e12 ps/s.
+ * mul_u64_u64_div_u64() avoids overflow when abs_ps approaches
+ * one second of 1 PPS wrap-around.
+ */
+ coarse = mul_u64_u64_div_u64(abs_ps, fvco, 1000000000000ULL);
+ if (coarse >= (1ULL << SIT9531X_OUT_PRG_COARSE_BITS))
+ return -ERANGE;
+
+ /* Fine delay = round((abs_ps - coarse * vco_period_ps) / 30 ps) */
+ coarse_ps = mul_u64_u64_div_u64(coarse, 1000000000000ULL, fvco);
+ rem_ps = (abs_ps > coarse_ps) ? (abs_ps - coarse_ps) : 0;
+ if (rem_ps) {
+ u64 steps;
+
+ steps = div64_u64(rem_ps + SIT9531X_OUT_PRG_FINE_STEP_PS / 2,
+ SIT9531X_OUT_PRG_FINE_STEP_PS);
+ if (steps > SIT9531X_OUT_PRG_FINE_MAX)
+ steps = SIT9531X_OUT_PRG_FINE_MAX;
+ fine = (u8)steps;
+ }
+
+ /*
+ * Map logical output index to the chip's physical output slot.
+ * On SiT95317 the eight logical outputs land on chip slots
+ * {0, 3, 4, 5, 7, 8, 9, 11}; on SiT95316 the map is identity.
+ * Page/base must address the slot, not the logical index.
+ */
+ slot = info->clkout_map[out_idx];
+ page = (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX) ?
+ SIT9531X_PAGE_OUTSYS1 : SIT9531X_PAGE_OUTSYS0;
+ base = SIT9531X_OUT_PRG_DELAY_BASE +
+ SIT9531X_OUT_PRG_SLOT_STRIDE * (slot % 6);
+
+ /* Caller (dpll.c) holds multiop_lock around the whole sequence. */
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ /*
+ * The PRG_RST_DELAY bytes live in the output system, so the writes
+ * only take effect when made inside the PRG_CMD programming state and
+ * committed to the NVM shadow, exactly like sit9531x_output_freq_set().
+ */
+ rc = sit9531x_prg_enter(sitdev);
+ if (rc)
+ return rc;
+
+ /* PROG6 RMW: preserve OPSTG_VCASC_BUMP in [7:5] */
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG(page, base),
+ &prog6_val);
+ if (rc)
+ goto commit;
+
+ prog6_val &= SIT9531X_OUT_PRG_OPSTG_MASK;
+ prog6_val |= (fine << SIT9531X_OUT_PRG_FINE_SHIFT) &
+ SIT9531X_OUT_PRG_FINE_MASK;
+ prog6_val |= (u8)((coarse >> 32) & SIT9531X_OUT_PRG_COARSE_HI_MASK);
+
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base),
+ prog6_val);
+ if (rc)
+ goto commit;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base + 1),
+ (u8)((coarse >> 24) & 0xFF));
+ if (rc)
+ goto commit;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base + 2),
+ (u8)((coarse >> 16) & 0xFF));
+ if (rc)
+ goto commit;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base + 3),
+ (u8)((coarse >> 8) & 0xFF));
+ if (rc)
+ goto commit;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG(page, base + 4),
+ (u8)(coarse & 0xFF));
+
+commit:
+ /*
+ * Always leave the PRG_CMD state via prg_commit(), even on a
+ * mid-sequence write failure, so the output loops are re-locked rather
+ * than stranded unlocked; keep the first error.
+ */
+ ret = sit9531x_prg_commit(sitdev);
+ if (ret && !rc)
+ rc = ret;
+ if (rc)
+ return rc;
+
+ /*
+ * Restart the output divider phase so the freshly programmed delay is
+ * applied against a known edge instead of the divider's arbitrary
+ * running phase.
+ */
+ return sit9531x_output_phase_flush(sitdev, pll_idx);
+}
+
+/* ====================================================================
+ * Notification clear
+ * ====================================================================
+ */
+
+/**
+ * sit9531x_clear_notifications - clear all notification registers
+ * @sitdev: device pointer
+ *
+ * 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.
+ *
+ * Return: 0 on success, <0 on error
+ */
+int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
+{
+ int rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ /* Page 0x00 W1C notification registers */
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_OUTER_LOL_NOTIF, 0xFF);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_HO_FREEZE_NOTIF, 0xFF);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PLL_INNER_LOL_NOTIF, 0xFF);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_CMON_NOTIF, 0xFF);
+ if (rc)
+ return rc;
+
+ /* Page 0x06 clock input monitor notifications */
+ rc = sit9531x_write_u8(sitdev, SIT9531X_CLKMON_P_NOTIF_01, 0xFF);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_CLKMON_P_NOTIF_23, 0xFF);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_CLKMON_N_NOTIF_01, 0xFF);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_CLKMON_N_NOTIF_23, 0xFF);
+ if (rc)
+ return rc;
+
+ dev_dbg(sitdev->dev, "All notification registers cleared\n");
+ return 0;
+}
+
+/* ====================================================================
+ * INTSYNC (inter-PLL synchronization)
+ * ====================================================================
+ */
+
+/*
+ * 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
+ * @sitdev: device pointer
+ * @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.
+ *
+ * Return: 0 on success, <0 on error
+ */
+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);
+
+ /* Set INTSYNC global enable bit (Page 0, reg 0x40 bit 6) */
+ 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_PLL_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;
+
+ /* Write INTSYNC configuration */
+ for (i = 0; i < ARRAY_SIZE(intsync_config); i++) {
+ rc = sit9531x_write_u8(sitdev,
+ SIT9531X_REG(ext_page,
+ intsync_config[i].offset),
+ intsync_config[i].en_val);
+ if (rc)
+ return rc;
+ }
+
+ /* Small update on source PLL */
+ rc = sit9531x_write_pll_u8(sitdev, src_pll_idx,
+ SIT9531X_PLL_REG_SMALL_UPDATE,
+ SIT9531X_PLL_SMALL_UPDATE_CMD);
+ if (rc)
+ return rc;
+
+ dev_info(sitdev->dev, "INTSYNC enabled on PLL%c\n",
+ 'A' + src_pll_idx);
+ return 0;
+}
+
+/**
+ * sit9531x_intsync_disable - disable inter-PLL synchronization
+ * @sitdev: device pointer
+ * @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.
+ *
+ * Return: 0 on success, <0 on error
+ */
+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);
+
+ /* Clear INTSYNC global enable bit */
+ 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_PLL_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;
+
+ /* Write INTSYNC configuration */
+ 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_PLL_SMALL_UPDATE_CMD);
+ if (rc)
+ return rc;
+
+ dev_info(sitdev->dev, "INTSYNC disabled on PLL%c\n",
+ 'A' + src_pll_idx);
+ return 0;
+}
+
+/**
+ * sit9531x_pll_sysref_mode_set - configure SYSREF / SYNCB / Pulser output mode
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0=PLLA ... 3=PLLD)
+ * @mode: one of sit9531x_sysref_mode (DISABLE, SYSREF, SYNCB, PULSER)
+ * @target_outputs: 12-bit one-hot mask selecting which physical chip
+ * slots (0..11) are driven by this PLL's SYSREF/SYNCB
+ *
+ * Programs PLL_CONFIG47_PLL bits 6/5/4 (DIVO_PULSER_MODE / DIVO_SYSREF_MODE
+ * / DIVO_SYNCB_MODE) and bits 3:0 (DIVO_SYS_REF[11:8]) plus
+ * Sysref_sel_PLL (DIVO_SYS_REF[7:0]). The per-PLL small-change trigger
+ * enable on Page 0 reg 0x19 is set when mode != DISABLE and cleared when
+ * mode == DISABLE.
+ *
+ * The mask is one-hot per physical slot. Callers needing to convert
+ * from logical output index must do so via info->clkout_map[].
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, <0 on error.
+ */
+int sit9531x_pll_sysref_mode_set(struct sit9531x_dev *sitdev, u8 pll_idx,
+ enum sit9531x_sysref_mode mode,
+ u16 target_outputs)
+{
+ u8 mode_bits = 0;
+ int rc;
+
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+ if (target_outputs & ~0x0FFFU)
+ return -EINVAL;
+
+ switch (mode) {
+ case SIT9531X_SYSREF_DISABLE:
+ break;
+ case SIT9531X_SYSREF_MODE_SYSREF:
+ mode_bits = SIT9531X_PLL_SYSREF_MODE_BIT;
+ break;
+ case SIT9531X_SYSREF_MODE_SYNCB:
+ mode_bits = SIT9531X_PLL_SYSREF_SYNCB_BIT;
+ break;
+ case SIT9531X_SYSREF_MODE_PULSER:
+ mode_bits = SIT9531X_PLL_SYSREF_PULSER_BIT |
+ SIT9531X_PLL_SYSREF_MODE_BIT;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ /* PLL reg 0x47: mode bits [6:4] + DIVO_SYS_REF[11:8] in [3:0] */
+ rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_SYSREF_MODE,
+ SIT9531X_PLL_SYSREF_MODE_MASK |
+ SIT9531X_PLL_SYSREF_TARGET_HI_MASK,
+ mode_bits |
+ ((target_outputs >> 8) &
+ SIT9531X_PLL_SYSREF_TARGET_HI_MASK));
+ if (rc)
+ return rc;
+
+ /* PLL reg 0x48: DIVO_SYS_REF[7:0] */
+ rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_SYSREF_SEL,
+ target_outputs & 0xFF);
+ if (rc)
+ return rc;
+
+ /* Page 0 reg 0x19: enable small-change (SYSREF) trigger for this PLL */
+ {
+ u8 trig_bit = BIT(SIT9531X_DIVO_SYSREF_TRIG_BIT(pll_idx));
+ u8 val;
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_DIVO_TRIGGER_EN,
+ &val);
+ if (rc)
+ return rc;
+ if (mode == SIT9531X_SYSREF_DISABLE)
+ val &= ~trig_bit;
+ else
+ val |= trig_bit;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_DIVO_TRIGGER_EN,
+ val);
+ if (rc)
+ return rc;
+ }
+
+ /*
+ * Gate the SYSREF output buffer (PLL-page 0xD5/0xD6 bit 7). The mode,
+ * target and trigger above are not enough on their own: with the
+ * buffer disabled the pad stays silent. Enable both for any active
+ * mode, clear them on disable.
+ */
+ {
+ u8 oe = (mode == SIT9531X_SYSREF_DISABLE) ?
+ 0 : SIT9531X_PLL_SYSREF_OE_BIT;
+
+ rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_SYSREF_OE0,
+ SIT9531X_PLL_SYSREF_OE_BIT, oe);
+ if (rc)
+ return rc;
+ rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_SYSREF_OE1,
+ SIT9531X_PLL_SYSREF_OE_BIT, oe);
+ if (rc)
+ return rc;
+ }
+
+ dev_info(sitdev->dev,
+ "PLL%c sysref mode=%d target_outputs=0x%03x\n",
+ 'A' + pll_idx, mode, target_outputs);
+ return 0;
+}
+
+/**
+ * sit9531x_output_pulse_ctrl_set - program per-output PULSE_CTRL byte
+ * @sitdev: device pointer
+ * @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.
+ *
+ * Return: 0 on success, <0 on error.
+ */
+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;
+}
+
+/* ====================================================================
+ * Phase offset readback (TDC)
+ * ====================================================================
+ */
+
+/**
+ * sit9531x_phase_offset_read - read phase difference via TDC
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ * @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.
+ *
+ * Return: 0 on success, <0 on error
+ */
+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;
+
+ /* Configure TDC */
+ rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_TDC_MODE,
+ SIT9531X_TDC_MODE_ENABLE);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_TDC_CFG,
+ SIT9531X_TDC_CFG_DEFAULT);
+ if (rc)
+ return rc;
+
+ /* Trigger TDC sampling by reading the trigger register 3 times */
+ for (i = 0; i < 3; i++) {
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_TDC_TRIGGER, &v);
+ if (rc)
+ return rc;
+ }
+
+ /* Read 40-bit TDC code (5 bytes: 0xB5-0xB9) */
+ tdc_raw = 0;
+
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_TDC_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_TDC_DATA_3, &v);
+ if (rc)
+ return rc;
+ tdc_raw |= (u64)v << 24;
+
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_TDC_DATA_2, &v);
+ if (rc)
+ return rc;
+ tdc_raw |= (u64)v << 16;
+
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_TDC_DATA_1, &v);
+ if (rc)
+ return rc;
+ tdc_raw |= (u64)v << 8;
+
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_TDC_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;
+}
+
+/* ====================================================================
+ * Hardware state fetch
+ * ====================================================================
+ */
+
+/*
+ * sit9531x_ref_state_fetch - read input reference status from hardware
+ * @sitdev: device pointer
+ * @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]).
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int sit9531x_ref_state_fetch(struct sit9531x_dev *sitdev, u8 index)
+{
+ struct sit9531x_ref *ref;
+ unsigned int reg;
+ u8 pair, status;
+ int rc;
+
+ /*
+ * The XTAL/XO reference (index SIT9531X_MAX_INPUTS) is the on-chip
+ * oscillator that feeds every PLL. It cannot be routed or deselected,
+ * so its pin is modelled as permanently connected (see
+ * sit9531x_dpll_xo_pin_ops) and its LOS/OOF flags are never consulted.
+ * Only the routable per-lane inputs (0..num_inputs-1) are polled here.
+ */
+ if (index >= SIT9531X_MAX_INPUTS)
+ return -EINVAL;
+
+ ref = &sitdev->ref[index];
+ pair = sit9531x_input_pair(index);
+
+ if (sit9531x_input_is_n(index))
+ reg = pair < 2 ? SIT9531X_CLKMON_N_STATUS_01
+ : SIT9531X_CLKMON_N_STATUS_23;
+ else
+ reg = pair < 2 ? SIT9531X_CLKMON_P_STATUS_01
+ : SIT9531X_CLKMON_P_STATUS_23;
+
+ rc = sit9531x_read_u8(sitdev, reg, &status);
+ if (rc)
+ return rc;
+
+ if (pair & 1)
+ status >>= 4;
+
+ ref->los = !!(status & (BIT(SIT9531X_CLKMON_CLK_LOSS) |
+ BIT(SIT9531X_CLKMON_CLK_LOSS_FD)));
+ ref->oof = !!(status & (BIT(SIT9531X_CLKMON_FREQ_FINE) |
+ BIT(SIT9531X_CLKMON_FREQ_COARSE)));
+
+ return 0;
+}
+
+/*
+ * sit9531x_input_mode_fetch - detect SE/DE configuration of an input pair
+ * @sitdev: device pointer
+ * @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.
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int sit9531x_input_mode_fetch(struct sit9531x_dev *sitdev, u8 pair)
+{
+ enum sit9531x_signal_mode sig_mode;
+ u8 mode;
+ int rc;
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_IN_MODE(pair), &mode);
+ if (rc)
+ return rc;
+
+ if (mode & (SIT9531X_IN_MODE_SE_P_EN | SIT9531X_IN_MODE_SE_N_EN))
+ sig_mode = SIT9531X_MODE_SE;
+ else
+ sig_mode = SIT9531X_MODE_DE;
+
+ sitdev->ref[pair * 2].sig_mode = sig_mode;
+ sitdev->ref[pair * 2 + 1].sig_mode = sig_mode;
+
+ dev_dbg(sitdev->dev, "CLKIN%u mode reg 0x%02x -> %s\n", pair, mode,
+ sig_mode == SIT9531X_MODE_DE ? "differential" : "single-ended");
+
+ return 0;
+}
+
+/*
+ * sit9531x_chan_state_fetch - read PLL channel status from hardware
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ *
+ * Reads lock status and mode from the PLL status register.
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int sit9531x_chan_state_fetch(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+ u8 status, holdover, input_sel, inner_lol, ho_freeze, activesel_reg;
+ struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+ int rc;
+
+ /* Read PLL lock/mode from PLL page */
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_STATUS, &status);
+ if (rc)
+ return rc;
+
+ /* Read holdover status from Page 0 */
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_HOLDOVER_STATUS, &holdover);
+ 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;
+
+ /* Read PLL inner loop loss-of-lock */
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_PLL_INNER_LOL_STATUS,
+ &inner_lol);
+ if (rc)
+ return rc;
+
+ /* Read holdover freeze status */
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_HO_FREEZE_STATUS, &ho_freeze);
+ if (rc)
+ return rc;
+
+ /*
+ * Holdover bit set means the PLL is in holdover -- i.e. NOT locked
+ * to its input. Invert the polarity so chan->locked reflects the
+ * intuitive sense (true == locked, false == holdover / free-run).
+ */
+ chan->locked = !(holdover & BIT(pll_idx));
+ chan->mode = !!(status & SIT9531X_PLL_STATUS_OUTER_DIS);
+ chan->selected_ref =
+ sit9531x_hw_src_input(input_sel & SIT9531X_PRIO_NIBBLE_MASK);
+ chan->inner_lol = !!(inner_lol & BIT(pll_idx));
+ chan->ho_freeze = !!(ho_freeze & BIT(pll_idx));
+
+ return 0;
+}
+
+/*
+ * sit9531x_out_state_fetch - read output status from hardware
+ * @sitdev: device pointer
+ * @index: output index
+ *
+ * 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.
+ *
+ * Return: 0 on success, <0 on error
+ */
+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
+ * @sitdev: device pointer
+ *
+ * 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.
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int sit9531x_ref_pll_mask_fetch(struct sit9531x_dev *sitdev)
+{
+ u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+ u8 pll_idx, slot, index;
+ int rc;
+
+ for (pll_idx = 0; pll_idx < SIT9531X_NUM_PLLS; pll_idx++) {
+ rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+ if (rc)
+ return rc;
+
+ for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+ index = sit9531x_hw_src_input(srcs[slot]);
+ if (index >= sitdev->info->num_inputs)
+ continue;
+
+ /*
+ * On a differentially configured pair only the P lane
+ * has a DPLL pin, so that is the lane the connect and
+ * disconnect callbacks account for. Fold an N-lane
+ * table entry onto its P lane, otherwise the count
+ * would land on a lane nothing ever releases. The
+ * signalling mode is already known here:
+ * sit9531x_input_mode_fetch() runs first.
+ */
+ if (sit9531x_input_is_n(index) &&
+ sitdev->ref[index].sig_mode == SIT9531X_MODE_DE)
+ index--;
+
+ sitdev->ref[index].pll_mask |= BIT(pll_idx);
+ }
+ }
+
+ return 0;
+}
+
+/*
+ * sit9531x_dev_state_fetch - read all hardware state at startup
+ * @sitdev: device pointer
+ *
+ * Called once during probe to populate the initial state cache.
+ *
+ * Return: 0 on success, <0 on error
+ */
+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
+ * serialised 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;
+}
+
+/* ====================================================================
+ * Periodic work thread
+ * ====================================================================
+ */
+
+/*
+ * sit9531x_dev_ref_states_update - update all input reference states
+ * @sitdev: device pointer
+ */
+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);
+ }
+}
+
+/*
+ * sit9531x_dev_chan_states_update - update all PLL channel states
+ * @sitdev: device pointer
+ */
+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 serialised 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);
+
+ /* Check for state changes on each registered DPLL */
+ 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 serialise 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;
+}
+
+/* ====================================================================
+ * Device lifecycle -- start / stop / dpll_init / dpll_fini
+ * ====================================================================
+ */
+
+/**
+ * sit9531x_dev_start - start normal operation
+ * @sitdev: device pointer
+ *
+ * Fetches initial hardware state, registers all DPLL devices and
+ * their pins, and starts the periodic monitoring thread.
+ *
+ * Return: 0 on success, <0 on error
+ */
+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;
+
+ /* Register all DPLLs */
+ 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);
+ return rc;
+ }
+ }
+
+ /* Start monitoring */
+ kthread_queue_delayed_work(sitdev->kworker, &sitdev->work, 0);
+
+ return 0;
+}
+
+/**
+ * sit9531x_dev_stop - stop normal operation
+ * @sitdev: device pointer
+ *
+ * Cancels the monitoring thread and unregisters all DPLL devices
+ * and their pins.
+ */
+void sit9531x_dev_stop(struct sit9531x_dev *sitdev)
+{
+ struct sit9531x_dpll *sitdpll;
+
+ /* Stop monitoring */
+ kthread_cancel_delayed_work_sync(&sitdev->work);
+
+ /* Unregister all DPLLs */
+ list_for_each_entry(sitdpll, &sitdev->dplls, list) {
+ if (sitdpll->dpll_dev)
+ sit9531x_dpll_unregister(sitdpll);
+ }
+}
+
+static void sit9531x_dev_dpll_fini(void *ptr)
+{
+ struct sit9531x_dpll *sitdpll, *next;
+ struct sit9531x_dev *sitdev = ptr;
+
+ /* Stop monitoring and unregister DPLLs */
+ sit9531x_dev_stop(sitdev);
+
+ /* Destroy monitoring thread */
+ if (sitdev->kworker) {
+ kthread_destroy_worker(sitdev->kworker);
+ sitdev->kworker = NULL;
+ }
+
+ /* Free all DPLLs */
+ list_for_each_entry_safe(sitdpll, next, &sitdev->dplls, list) {
+ list_del(&sitdpll->list);
+ sit9531x_dpll_free(sitdpll);
+ }
+}
+
+/*
+ * sit9531x_devm_dpll_init - allocate DPLLs and start the device
+ * @sitdev: device pointer
+ *
+ * Allocates one DPLL per PLL channel, creates the monitoring thread,
+ * starts normal operation, and registers a devres cleanup action.
+ *
+ * Return: 0 on success, <0 on error
+ */
+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);
+
+ /*
+ * Initialize the monitoring work before anything that can fail into
+ * the error path: sit9531x_dev_dpll_fini() -> sit9531x_dev_stop()
+ * calls kthread_cancel_delayed_work_sync(&sitdev->work)
+ * unconditionally, which must not run on an uninitialized work.
+ */
+ kthread_init_delayed_work(&sitdev->work, sit9531x_dev_periodic_work);
+
+ /* Allocate all DPLLs */
+ for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+ sitdpll = sit9531x_dpll_alloc(sitdev, i);
+ if (IS_ERR(sitdpll)) {
+ dev_err_probe(sitdev->dev, PTR_ERR(sitdpll),
+ "Failed to alloc DPLL%u\n", i);
+ rc = PTR_ERR(sitdpll);
+ goto error;
+ }
+
+ list_add_tail(&sitdpll->list, &sitdev->dplls);
+ }
+
+ /* Start the monitoring thread worker */
+ kworker = kthread_run_worker(0, "sit9531x-%s",
+ dev_name(sitdev->dev));
+ if (IS_ERR(kworker)) {
+ rc = PTR_ERR(kworker);
+ goto error;
+ }
+ sitdev->kworker = kworker;
+
+ /* Start normal operation */
+ rc = sit9531x_dev_start(sitdev);
+ if (rc) {
+ dev_err_probe(sitdev->dev, rc, "Failed to start device\n");
+ goto error;
+ }
+
+ /* Add devres action to release DPLL related resources */
+ return devm_add_action_or_reset(sitdev->dev, sit9531x_dev_dpll_fini,
+ sitdev);
+
+error:
+ sit9531x_dev_dpll_fini(sitdev);
+
+ return rc;
+}
+
+/* ====================================================================
+ * Chip identification
+ * ====================================================================
+ */
+
+/*
+ * sit9531x_read_variant_id - read chip variant ID byte from hardware
+ * @sitdev: device pointer
+ * @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.
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int sit9531x_read_variant_id(struct sit9531x_dev *sitdev, u8 *id)
+{
+ return sit9531x_read_u8(sitdev, SIT9531X_REG_VARIANT_ID, id);
+}
+
+/*
+ * sit9531x_match_variant - match variant ID against known variants
+ * @id: variant ID byte
+ *
+ * Return: pointer to chip_info on match, NULL on unknown 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
+ * @sitdev: device pointer
+ *
+ * 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;
+}
+
+/* ====================================================================
+ * Probe entry point
+ * ====================================================================
+ */
+
+/**
+ * sit9531x_dev_probe - initialize SiT9531x device
+ * @sitdev: pointer to device structure (caller-allocated)
+ *
+ * Common initialization: read chip ID, match variant, generate
+ * clock_id, initialize synchronization mutex, and register DPLL
+ * channels. Called from the I2C probe function.
+ *
+ * Return: 0 on success, <0 on error
+ */
+int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
+{
+ struct clk *xtal_clk;
+ u8 variant_id;
+ int rc;
+
+ /*
+ * Read the external reference (XO) feeding the chip's XIN/XO_CLK
+ * input. Required: Fvco computation does
+ * Fvco = Fref * (DIVN + frac/2^32) with Fref = xtal_freq << doubler,
+ * so without a populated xtal_freq every freq_set/phase_adjust path
+ * dividing by Fvco fails with -EIO. The rate normally comes from a
+ * "clocks" phandle (clock-names = "xtal"). As a fallback, when the
+ * firmware does not yet expose the XO through the clock framework,
+ * take the rate from a "clock-frequency" device property instead.
+ */
+ xtal_clk = devm_clk_get_optional_enabled(sitdev->dev, "xtal");
+ if (IS_ERR(xtal_clk))
+ return dev_err_probe(sitdev->dev, PTR_ERR(xtal_clk),
+ "Failed to get xtal clock\n");
+ sitdev->xtal_freq = xtal_clk ? clk_get_rate(xtal_clk) : 0;
+ if (!sitdev->xtal_freq) {
+ u32 freq;
+
+ if (!device_property_read_u32(sitdev->dev, "clock-frequency",
+ &freq))
+ sitdev->xtal_freq = freq;
+ }
+ if (!sitdev->xtal_freq)
+ return dev_err_probe(sitdev->dev, -EINVAL,
+ "no xtal rate: provide clocks=<&xo> + clock-names=\"xtal\", or a clock-frequency property\n");
+ dev_info(sitdev->dev, "xtal_freq=%u Hz\n", sitdev->xtal_freq);
+
+ /*
+ * Optional DT-described reset line. Requested in the deasserted
+ * state so any prior chip programming is not torn down by the
+ * request itself. The driver deliberately never pulses reset at
+ * runtime: the chip configuration (filter coefficients, output
+ * routing, priority tables) is loaded from efuse or an NVM blob
+ * before probe, and a hardware reset would discard it. The line
+ * is held deasserted only to guarantee the chip is out of reset
+ * for I2C. Absent DT property == descriptor stays NULL, no
+ * behaviour change.
+ *
+ * Must run before the first I2C access: if the board wires reset
+ * and starts with the line asserted, the chip is held in reset and
+ * variant-ID reads return -EIO/-ETIMEDOUT.
+ */
+ 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) {
+ dev_info(sitdev->dev, "reset-gpios: present (held deasserted)\n");
+ /*
+ * If the board powered up with RESETB asserted, requesting
+ * the line deasserted above just released the chip. Wait for
+ * its internal boot to complete before the first I2C access so
+ * sit9531x_read_variant_id() below does not race chip
+ * readiness and return -EIO/-ETIMEDOUT.
+ */
+ fsleep(10000);
+ }
+
+ /*
+ * Optional board-config overrides for fixed (efuse/blob) routing
+ * that the chip registers do not expose unambiguously. Absent
+ * properties leave pll_fvco[] zeroed (derive from DIVN) and
+ * out_pll_map_valid false (use the OUT_MAP registers).
+ */
+ 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")) {
+ u32 map[SIT9531X_MAX_OUTPUTS];
+ int n, i;
+
+ /*
+ * Accept any 1..MAX_OUTPUTS length so the 8-output SiT95317
+ * can supply an 8-entry map instead of being forced to pad to
+ * 12. Variant detection has not run yet; entries beyond the
+ * detected num_outputs are simply never indexed.
+ *
+ * Default every entry to "unmapped" first: when a shorter map
+ * is supplied for a variant with more outputs, the trailing
+ * entries must not read back as 0 (== PLLA) and falsely mark
+ * unrouted outputs as 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)) {
+ for (i = 0; i < n; i++)
+ sitdev->out_pll_map[i] = map[i];
+ sitdev->out_pll_map_valid = true;
+ }
+ }
+
+ /* Read variant ID byte */
+ rc = sit9531x_read_variant_id(sitdev, &variant_id);
+ if (rc)
+ return rc;
+
+ /* Detect chip variant */
+ 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);
+
+ dev_info(sitdev->dev, "VariantID(0x%02X), %s (%u in, %u out)\n",
+ variant_id, sitdev->info->name,
+ sitdev->info->num_inputs, sitdev->info->num_outputs);
+
+ /* Generate deterministic clock ID */
+ sitdev->clock_id = sit9531x_derive_clock_id(sitdev);
+
+ /* No PLL sources inter-PLL synchronization until requested */
+ sitdev->intsync_src = -1;
+
+ /* Initialize mutex for multi-register atomic operations */
+ rc = devm_mutex_init(sitdev->dev, &sitdev->multiop_lock);
+ if (rc)
+ return dev_err_probe(sitdev->dev, rc,
+ "Failed to initialize mutex\n");
+
+ /*
+ * Register DPLL channels and create the kworker first. The IRQ
+ * handler dereferences sitdev->kworker via
+ * kthread_mod_delayed_work(), so it must be live before any
+ * INTRB assertion can land on the request_threaded_irq path.
+ */
+ rc = sit9531x_devm_dpll_init(sitdev);
+ if (rc)
+ return rc;
+
+ /*
+ * Optional INTRB IRQ from DT. The I2C subsystem populates
+ * client->irq from the node's "interrupts"/"interrupts-extended"
+ * property; if no IRQ is wired client->irq is 0 and we keep
+ * relying on the periodic poll.
+ */
+ 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);
+ dev_info(sitdev->dev,
+ "INTRB IRQ %d wired (threaded handler kicks periodic poll)\n",
+ sitdev->irq);
+ }
+
+ return 0;
+}
+
+/* ====================================================================
+ * I2C driver
+ * ====================================================================
+ */
+
+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 i2c_device_id sit9531x_i2c_id[] = {
+ { "sit95317" },
+ { "sit95316" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, sit9531x_i2c_id);
+
+static const struct of_device_id sit9531x_of_match[] = {
+ { .compatible = "sitime,sit95317" },
+ { .compatible = "sitime,sit95316" },
+ { }
+};
+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,
+ .id_table = sit9531x_i2c_id,
+};
+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..48adeaa8453e
--- /dev/null
+++ b/drivers/dpll/sit9531x/core.h
@@ -0,0 +1,388 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * SiTime SiT9531x DPLL core driver
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * Device structure, register access helpers, and core function
+ * declarations.
+ */
+
+#ifndef _SIT9531X_CORE_H
+#define _SIT9531X_CORE_H
+
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/kthread.h>
+#include <linux/list.h>
+#include <linux/mutex.h>
+#include <linux/regmap.h>
+#include <linux/types.h>
+
+#include "regs.h"
+
+#define SIT9531X_NUM_PLLS 4
+#define SIT9531X_MAX_INPUTS 8
+#define SIT9531X_NUM_INPUT_PAIRS (SIT9531X_MAX_INPUTS / 2)
+#define SIT9531X_MAX_OUTPUTS 12
+/* out_pll_map[] entry meaning "this output is not routed to any PLL" */
+#define SIT9531X_OUT_PLL_UNMAPPED 0xFF
+/*
+ * INTSYNC (the inter-PLL sync net) is modelled as two pins. The
+ * destination PLL that locks to INTSYNC sees an input pin
+ * (SIT9531X_INTSYNC_PIN_ID, in the input id namespace after the physical
+ * inputs and the xtal); the source PLL that drives INTSYNC sees an output
+ * pin (SIT9531X_INTSYNC_OUT_PIN_ID, appended after the physical outputs).
+ */
+#define SIT9531X_INTSYNC_PIN_ID (SIT9531X_MAX_INPUTS + 1)
+#define SIT9531X_INTSYNC_OUT_PIN_ID SIT9531X_MAX_OUTPUTS
+#define SIT9531X_NUM_PINS (SIT9531X_MAX_INPUTS + 2 + SIT9531X_MAX_OUTPUTS + 1)
+#define SIT9531X_STATUS_POLL_MS 500
+
+/* selected_ref value when the active source is not a registered input */
+#define SIT9531X_REF_INVALID 0xFF
+
+/* SiTime IEEE OUI for EUI-64 generation */
+#define SIT9531X_OUI 0x0090C2FFFEULL
+
+struct sit9531x_dpll;
+
+/**
+ * struct sit9531x_chip_info - chip variant identification
+ * @id: variant ID byte read from register
+ * @num_inputs: number of input clock pins
+ * @num_outputs: number of output clock pins
+ * @name: human-readable variant name
+ * @clkout_map: per-output slot mapping (output index -> physical slot)
+ */
+struct sit9531x_chip_info {
+ u8 id;
+ u8 num_inputs;
+ u8 num_outputs;
+ const char *name;
+ const u8 *clkout_map;
+};
+
+/**
+ * enum sit9531x_signal_mode - input signal electrical mode
+ * @SIT9531X_MODE_SE: single-ended
+ * @SIT9531X_MODE_DE: differential
+ */
+enum sit9531x_signal_mode {
+ SIT9531X_MODE_SE = 0,
+ SIT9531X_MODE_DE,
+};
+
+/**
+ * struct sit9531x_ref - input reference state
+ * @freq: configured frequency in Hz
+ * @enabled: reference is enabled for monitoring
+ * @los: loss-of-signal detected
+ * @oof: out-of-frequency detected
+ * @pll_mask: bitmask of PLLs this input feeds (bit 0 = PLLA)
+ * @label: board label from DT or default
+ * @sig_mode: signal mode of the pair this lane belongs to
+ * (detected from CLKINx_INPUT_MODE at probe)
+ */
+struct sit9531x_ref {
+ u32 freq;
+ bool enabled;
+ bool los;
+ bool oof;
+ u8 pll_mask;
+ const char *label;
+ enum sit9531x_signal_mode sig_mode;
+};
+
+/**
+ * struct sit9531x_out - output state
+ * @freq: current output 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
+ * @locked: PLL is locked (raw status register bit)
+ * @mode: 0 = sync (outer loop enabled), 1 = free-run
+ * @selected_ref: logical input index of the currently selected
+ * reference (the INTSYNC net maps to
+ * SIT9531X_INTSYNC_PIN_ID), or SIT9531X_REF_INVALID
+ * when the hardware source encoding is reserved
+ * @inner_lol: PLL inner loop loss-of-lock detected
+ * @ho_freeze: holdover freeze active
+ */
+struct sit9531x_chan {
+ bool locked;
+ u8 mode;
+ u8 selected_ref;
+ bool inner_lol;
+ bool ho_freeze;
+};
+
+/**
+ * struct sit9531x_dev - SiT9531x device instance
+ * @dev: pointer to device
+ * @client: I2C client
+ * @regmap: regmap for register access
+ * @info: detected chip variant info
+ * @multiop_lock: mutex for multi-register atomic operations
+ * @ref: array of input reference states
+ * @out: array of output states
+ * @chan: array of per-PLL channel states
+ * @xtal_freq: crystal oscillator frequency in Hz
+ * @dplls: list of registered DPLL devices
+ * @kworker: kthread worker for periodic polling
+ * @work: delayed work for periodic state checks
+ * @clock_id: IEEE 1588 EUI-64 clock identifier
+ * @reset_gpio: optional reset line (DT "reset-gpios"), NULL if absent
+ * @irq: optional INTRB IRQ number (from DT "interrupts" via the
+ * I2C client), 0 if no IRQ is wired
+ * @pll_fvco: optional per-PLL VCO in Hz from DT
+ * "sitime,pll-fvco"; 0 means derive from DIVN
+ * @out_pll_map: optional per-output source PLL (0-3, 0xff =
+ * unmapped) from DT "sitime,output-pll-map"
+ * @out_pll_map_valid: true when out_pll_map[] was populated from DT;
+ * false means use the chip's OUT_MAP registers
+ * @intsync_src: PLL index currently sourcing inter-PLL
+ * synchronization (INTSYNC), or -1 when disabled
+ */
+struct sit9531x_dev {
+ struct device *dev;
+ struct i2c_client *client;
+ struct regmap *regmap;
+ const struct sit9531x_chip_info *info;
+ /* Serialises multi-step register sequences */
+ struct mutex multiop_lock;
+
+ /* Hardware state */
+ struct sit9531x_ref ref[SIT9531X_MAX_INPUTS + 1]; /* +1 for xtal */
+ struct sit9531x_out out[SIT9531X_MAX_OUTPUTS];
+ struct sit9531x_chan chan[SIT9531X_NUM_PLLS];
+ u32 xtal_freq;
+
+ /* DPLL channels */
+ struct list_head dplls;
+
+ /* Monitor */
+ struct kthread_worker *kworker;
+ struct kthread_delayed_work work;
+
+ /* Device identity */
+ u64 clock_id;
+
+ /* Optional DT-described GPIO / IRQ lines */
+ struct gpio_desc *reset_gpio;
+ int irq;
+
+ /* Optional DT board-config overrides */
+ u64 pll_fvco[SIT9531X_NUM_PLLS];
+ u8 out_pll_map[SIT9531X_MAX_OUTPUTS];
+ bool out_pll_map_valid;
+
+ /* Inter-PLL synchronization state */
+ s8 intsync_src;
+};
+
+extern const struct regmap_config sit9531x_regmap_config;
+
+/* ---- Core lifecycle ---- */
+int sit9531x_dev_probe(struct sit9531x_dev *sitdev);
+int sit9531x_dev_start(struct sit9531x_dev *sitdev);
+void sit9531x_dev_stop(struct sit9531x_dev *sitdev);
+
+/* ---- Register access ---- */
+int sit9531x_read_u8(struct sit9531x_dev *sitdev, unsigned int reg,
+ u8 *val);
+int sit9531x_write_u8(struct sit9531x_dev *sitdev, unsigned int reg,
+ u8 val);
+int sit9531x_read_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 offset, u8 *val);
+int sit9531x_write_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 offset, u8 val);
+int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 offset, u8 mask, u8 val);
+
+/* ---- Input enable/disable ---- */
+int sit9531x_input_disable(struct sit9531x_dev *sitdev, u8 index);
+int sit9531x_input_enable(struct sit9531x_dev *sitdev, u8 index);
+
+/* ---- Input priority ---- */
+int sit9531x_input_prio_set(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 input_idx, u8 prio);
+int sit9531x_input_prio_get(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 input_idx, u8 *prio);
+int sit9531x_input_prio_remove(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 input_idx);
+int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 input_idx);
+
+/* ---- Output enable/disable (Hi-Z control) ---- */
+int sit9531x_output_disable(struct sit9531x_dev *sitdev, u8 index);
+int sit9531x_output_enable(struct sit9531x_dev *sitdev, u8 index);
+
+/* ---- DCO tuning ---- */
+int sit9531x_dco_adjust(struct sit9531x_dev *sitdev, u8 pll_idx, s64 ppb);
+
+/* ---- 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);
+
+/* ---- Per-PLL SYSREF / SYNCB / Pulser output mode ---- */
+enum sit9531x_sysref_mode {
+ SIT9531X_SYSREF_DISABLE,
+ SIT9531X_SYSREF_MODE_SYSREF,
+ SIT9531X_SYSREF_MODE_SYNCB,
+ SIT9531X_SYSREF_MODE_PULSER,
+};
+
+int sit9531x_pll_sysref_mode_set(struct sit9531x_dev *sitdev, u8 pll_idx,
+ enum sit9531x_sysref_mode mode,
+ u16 target_outputs);
+int sit9531x_output_pulse_ctrl_set(struct sit9531x_dev *sitdev,
+ u8 out_idx, u8 pulse_ctrl);
+
+/* ---- Phase offset (TDC readback) ---- */
+int sit9531x_phase_offset_read(struct sit9531x_dev *sitdev, u8 pll_idx,
+ s64 *phase_ps);
+
+/* ---- State helpers ---- */
+
+/**
+ * sit9531x_pll_page - get register page for PLL index
+ * @pll_idx: PLL index (0 = PLLA, 3 = PLLD)
+ */
+static inline u8 sit9531x_pll_page(u8 pll_idx)
+{
+ return SIT9531X_PAGE_PLLA + pll_idx;
+}
+
+/*
+ * Logical input pins are interleaved: even index = P lane, odd
+ * index = N lane of pair index/2 (IN0P, IN0N, IN1P, IN1N, ...).
+ * Index SIT9531X_MAX_INPUTS is the XO input.
+ */
+
+/**
+ * sit9531x_input_pair - get input pair number for a logical input index
+ * @index: logical input pin index
+ */
+static inline u8 sit9531x_input_pair(u8 index)
+{
+ return index >> 1;
+}
+
+/**
+ * sit9531x_input_is_n - check if a logical input index is an N lane
+ * @index: logical input pin index
+ */
+static inline bool sit9531x_input_is_n(u8 index)
+{
+ return index & 1;
+}
+
+/**
+ * sit9531x_input_hw_src - translate logical input index to source encoding
+ * @index: logical input pin index
+ *
+ * The priority table and CLK_ACTIVESEL registers use a non-contiguous
+ * source encoding: 0-3 = CLK0P..CLK3P, 5 = OCXO, 6 = INTSYNC,
+ * 7-10 = CLK0N..CLK3N.
+ */
+static inline u8 sit9531x_input_hw_src(u8 index)
+{
+ if (index == SIT9531X_MAX_INPUTS)
+ return SIT9531X_PRIO_SRC_OCXO;
+ if (index == SIT9531X_INTSYNC_PIN_ID)
+ return SIT9531X_PRIO_SRC_INTSYNC;
+ if (sit9531x_input_is_n(index))
+ return SIT9531X_PRIO_SRC_N_BASE + sit9531x_input_pair(index);
+ return sit9531x_input_pair(index);
+}
+
+/**
+ * sit9531x_hw_src_input - translate source encoding to logical input index
+ * @src: 4-bit hardware source encoding
+ *
+ * Return: logical input index (INTSYNC maps to SIT9531X_INTSYNC_PIN_ID),
+ * or SIT9531X_REF_INVALID if @src is a reserved value
+ */
+static inline u8 sit9531x_hw_src_input(u8 src)
+{
+ if (src < SIT9531X_NUM_INPUT_PAIRS)
+ return src * 2;
+ if (src == SIT9531X_PRIO_SRC_OCXO)
+ return SIT9531X_MAX_INPUTS;
+ if (src == SIT9531X_PRIO_SRC_INTSYNC)
+ return SIT9531X_INTSYNC_PIN_ID;
+ if (src >= SIT9531X_PRIO_SRC_N_BASE &&
+ src < SIT9531X_PRIO_SRC_N_BASE + SIT9531X_NUM_INPUT_PAIRS)
+ return (src - SIT9531X_PRIO_SRC_N_BASE) * 2 + 1;
+ return SIT9531X_REF_INVALID;
+}
+
+/**
+ * sit9531x_ref_state_get - get reference state by index
+ * @sitdev: device pointer
+ * @index: logical input index
+ *
+ * Return: pointer to the cached input reference state
+ */
+static inline const struct sit9531x_ref *
+sit9531x_ref_state_get(const struct sit9531x_dev *sitdev, u8 index)
+{
+ return &sitdev->ref[index];
+}
+
+/**
+ * sit9531x_out_state_get - get output state by index
+ * @sitdev: device pointer
+ * @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
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ *
+ * 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..668d25b3c47a
--- /dev/null
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -0,0 +1,1580 @@
+// 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"
+
+/* 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_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;
+}
+
+/* ====================================================================
+ * Pin direction helpers
+ * ====================================================================
+ */
+
+static inline bool sit9531x_dpll_is_input_pin(const struct sit9531x_dpll_pin *pin)
+{
+ return pin->dir == DPLL_PIN_DIRECTION_INPUT;
+}
+
+static inline bool
+sit9531x_dpll_is_intsync_pin(const struct sit9531x_dpll_pin *pin)
+{
+ return sit9531x_dpll_is_input_pin(pin) &&
+ pin->id == SIT9531X_INTSYNC_PIN_ID;
+}
+
+static inline bool
+sit9531x_dpll_is_intsync_src_pin(const struct sit9531x_dpll_pin *pin)
+{
+ return !sit9531x_dpll_is_input_pin(pin) &&
+ pin->id == SIT9531X_INTSYNC_OUT_PIN_ID;
+}
+
+static inline bool
+sit9531x_dpll_is_xo_pin(const struct sit9531x_dpll_pin *pin)
+{
+ return sit9531x_dpll_is_input_pin(pin) &&
+ pin->id == SIT9531X_MAX_INPUTS;
+}
+
+/* ====================================================================
+ * dpll_device_ops callbacks
+ * ====================================================================
+ */
+
+/*
+ * sit9531x_dpll_lock_status_get - report PLL lock/holdover state
+ *
+ * reads holdover register (Page 0 reg 0x06), PLL
+ * status register (PLL page reg 0x31), inner LOL (reg 0x92), and
+ * holdover freeze (reg 0x0A) to determine lock status and error.
+ */
+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->locked) {
+ if (chan->mode)
+ *status = DPLL_LOCK_STATUS_LOCKED; /* free-run */
+ else
+ *status = DPLL_LOCK_STATUS_LOCKED_HO_ACQ; /* sync */
+ } else if (chan->ho_freeze) {
+ *status = DPLL_LOCK_STATUS_HOLDOVER;
+ } else {
+ *status = DPLL_LOCK_STATUS_UNLOCKED;
+ }
+
+ /* Report inner LOL as an error condition */
+ if (status_error && chan->inner_lol)
+ *status_error = DPLL_LOCK_STATUS_ERROR_UNDEFINED;
+
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_mode_get - report current PLL operating mode
+ *
+ * reads outer loop disable bit (PLL page reg 0x31[5]).
+ * Free-run -> MANUAL, sync -> AUTOMATIC.
+ */
+static int
+sit9531x_dpll_mode_get(const struct dpll_device *dpll, void *dpll_priv,
+ enum dpll_mode *mode, struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ const struct sit9531x_chan *chan;
+
+ chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
+
+ mutex_lock(&sitdev->multiop_lock);
+ *mode = chan->mode ? DPLL_MODE_MANUAL : DPLL_MODE_AUTOMATIC;
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_mode_set - switch PLL between free-run and sync mode
+ *
+ * writes PLL page reg 0x31[5] to enable/disable the
+ * outer loop, then triggers a small update via reg 0x0F.
+ *
+ * The driver maps AUTOMATIC to sync mode (outer loop enabled; the PLL
+ * selects its reference automatically from the priority table) and
+ * MANUAL to free-run (outer loop disabled via bit 0x31[5]). The chip
+ * also has a separate manual-active-select path (MANUAL_ACTIVE_SEL_PLL
+ * plus the per-PLL man_in_sel registers) that can pin one reference
+ * while still syncing; that path is not wired to a DPLL callback here.
+ */
+static int
+sit9531x_dpll_mode_set(const struct dpll_device *dpll, void *dpll_priv,
+ enum dpll_mode mode, struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ u8 val;
+ int rc;
+
+ /*
+ * Outer loop disable bit:
+ * 0 = sync mode (outer loop enabled) -> AUTOMATIC
+ * 1 = free-run (outer loop disabled) -> MANUAL
+ */
+ val = (mode == DPLL_MODE_MANUAL) ? SIT9531X_PLL_STATUS_OUTER_DIS : 0;
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ rc = sit9531x_update_pll_u8(sitdev, sitdpll->id,
+ SIT9531X_PLL_REG_STATUS,
+ SIT9531X_PLL_STATUS_OUTER_DIS, val);
+ if (rc) {
+ NL_SET_ERR_MSG(extack, "Failed to write PLL mode register");
+ goto unlock;
+ }
+
+ /* Trigger small update to apply without full NVM cycle */
+ rc = sit9531x_write_pll_u8(sitdev, sitdpll->id,
+ SIT9531X_PLL_REG_SMALL_UPDATE,
+ SIT9531X_PLL_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 sit9531x_dco_adjust() picks the
+ * inner- or outer-loop path from this cache and would otherwise
+ * act on the pre-switch mode until the next poll.
+ */
+ sitdev->chan[sitdpll->id].mode = !!val;
+
+unlock:
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return rc;
+}
+
+/*
+ * sit9531x_dpll_supported_modes_get - report which modes the PLL supports
+ */
+static int
+sit9531x_dpll_supported_modes_get(const struct dpll_device *dpll,
+ void *dpll_priv, unsigned long *modes,
+ struct netlink_ext_ack *extack)
+{
+ __set_bit(DPLL_MODE_AUTOMATIC, modes);
+ __set_bit(DPLL_MODE_MANUAL, modes);
+
+ return 0;
+}
+
+static 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 */
+};
+
+/* ====================================================================
+ * Input pin ops
+ * ====================================================================
+ */
+
+static int
+sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_direction *direction,
+ struct netlink_ext_ack *extack)
+{
+ *direction = DPLL_PIN_DIRECTION_INPUT;
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_input_pin_frequency_get - read input pin frequency
+ *
+ * returns cached frequency from DT or last set.
+ */
+static int
+sit9531x_dpll_input_pin_frequency_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, u64 *frequency,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ const struct sit9531x_ref *ref;
+
+ ref = sit9531x_ref_state_get(sitdpll->dev, dpin->id);
+ *frequency = ref->freq;
+
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_input_pin_state_on_dpll_get - get input pin DPLL state
+ *
+ * determines pin state from channel state: connected
+ * if this input is the selected reference on a locked PLL, selectable
+ * if enabled in automatic mode, disconnected otherwise.
+ */
+static int
+sit9531x_dpll_input_pin_state_on_dpll_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state *state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ const struct sit9531x_chan *chan;
+ const struct sit9531x_ref *ref;
+
+ chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
+ ref = sit9531x_ref_state_get(sitdev, dpin->id);
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ /*
+ * Report CONNECTED only when the PLL is genuinely locked to this
+ * input: not in holdover AND inner loop locked. Gating on the
+ * holdover bit alone let an idle PLL that happens not to be in
+ * holdover (e.g. an unused PLLD) falsely claim its default source
+ * (IN0P) as connected right after probe.
+ */
+ if (chan->locked && !chan->inner_lol &&
+ chan->selected_ref == dpin->id)
+ *state = DPLL_PIN_STATE_CONNECTED;
+ /* In auto mode, enabled inputs with good signal are selectable */
+ else if (!chan->mode && ref->enabled && !ref->los && !ref->oof)
+ *state = DPLL_PIN_STATE_SELECTABLE;
+ else
+ *state = DPLL_PIN_STATE_DISCONNECTED;
+
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_input_pin_state_on_dpll_set - set input pin DPLL state
+ *
+ * Enables or disables the physical input receiver via Page 0x02
+ * force/state registers (sit9531x_input_disable/enable()) and updates
+ * this DPLL's Page 1 priority table so the state is honoured by the
+ * PLL's automatic reference selection, not just at the input buffer.
+ * DISCONNECTED -> drop the input from this DPLL's priority table and
+ * release this DPLL's claim on the input
+ * SELECTABLE -> claim the input for this DPLL and make it eligible
+ * again in the priority table
+ * CONNECTED -> same as SELECTABLE (the PLL auto-switch logic does
+ * the actual selection)
+ *
+ * The priority table is per PLL, so it is always updated for this DPLL.
+ * A single physical input feeds every DPLL, so the hardware receiver is
+ * only cut off once the last DPLL has released it: ref->pll_mask tracks
+ * which DPLLs currently claim the input, and the physical disable
+ * happens on the transition to an empty mask.
+ */
+static int
+sit9531x_dpll_input_pin_state_on_dpll_set(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ struct sit9531x_ref *ref = &sitdev->ref[dpin->id];
+ u8 hw_src = sit9531x_input_hw_src(dpin->id);
+ u8 pll_bit = BIT(sitdpll->id);
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ switch (state) {
+ case DPLL_PIN_STATE_DISCONNECTED:
+ rc = sit9531x_input_prio_remove(sitdev, sitdpll->id, hw_src);
+ if (rc)
+ break;
+ ref->pll_mask &= ~pll_bit;
+ if (ref->pll_mask)
+ rc = 0; /* another DPLL still uses this input */
+ else
+ rc = sit9531x_input_disable(sitdev, dpin->id);
+ break;
+ case DPLL_PIN_STATE_SELECTABLE:
+ case DPLL_PIN_STATE_CONNECTED:
+ rc = sit9531x_input_enable(sitdev, dpin->id);
+ if (rc)
+ break;
+ rc = sit9531x_input_prio_add(sitdev, sitdpll->id, hw_src);
+ if (rc)
+ break;
+ /*
+ * Claim the input for this DPLL only once it is both enabled
+ * and present in the priority table. Setting the mask before
+ * prio_add would leak the claim if prio_add failed, keeping the
+ * shared input receiver powered even after every DPLL released
+ * it.
+ */
+ ref->pll_mask |= pll_bit;
+ break;
+ default:
+ rc = -EINVAL;
+ break;
+ }
+
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc)
+ NL_SET_ERR_MSG(extack, "Failed to set input pin state");
+
+ return rc;
+}
+
+/*
+ * sit9531x_dpll_input_pin_prio_get - read input pin priority
+ *
+ * reads the PLL's priority table on Page 1 (via
+ * sit9531x_input_prio_get()) and returns the slot the input
+ * occupies, so pin-get reports the real hardware priority rather
+ * than a software default.
+ */
+static int
+sit9531x_dpll_input_pin_prio_get(const struct dpll_pin *pin, void *pin_priv,
+ const struct dpll_device *dpll, void *dpll_priv,
+ u32 *prio, struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ u8 slot;
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_input_prio_get(sitdev, sitdpll->id,
+ sit9531x_input_hw_src(dpin->id), &slot);
+ mutex_unlock(&sitdev->multiop_lock);
+ if (rc)
+ return rc;
+
+ dpin->prio = slot;
+ *prio = slot;
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_input_pin_prio_set - set input pin priority
+ *
+ * writes input priority table on Page 1 via
+ * core.c sit9531x_input_prio_set(). Forces holdover during update.
+ */
+static int
+sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
+ const struct dpll_device *dpll, void *dpll_priv,
+ u32 prio, struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ int rc;
+
+ if (dpin->dir != DPLL_PIN_DIRECTION_INPUT) {
+ NL_SET_ERR_MSG(extack, "Priority applies only to input pins");
+ return -EINVAL;
+ }
+
+ if (prio >= SIT9531X_PRIO_MAX_SLOTS) {
+ NL_SET_ERR_MSG(extack, "Priority out of range (0-11)");
+ return -EINVAL;
+ }
+
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_input_prio_set(sitdev, sitdpll->id,
+ sit9531x_input_hw_src(dpin->id),
+ (u8)prio);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc) {
+ NL_SET_ERR_MSG(extack, "Failed to set input priority");
+ return rc;
+ }
+
+ dpin->prio = (u8)prio;
+
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_input_pin_phase_adjust_get - read phase adjustment
+ *
+ * returns cached phase adjustment value (in ps).
+ */
+static int
+sit9531x_dpll_input_pin_phase_adjust_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, s32 *phase_adjust,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+
+ *phase_adjust = dpin->phase_adjust;
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_input_pin_phase_offset_get - read phase offset
+ *
+ * reads the TDC (Time-to-Digital Converter) hardware
+ * to measure the phase difference in picoseconds via
+ * sit9531x_phase_offset_read().
+ */
+static int
+sit9531x_dpll_input_pin_phase_offset_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, s64 *phase_offset,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ s64 offset;
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ /*
+ * The on-chip TDC is a per-PLL resource that always measures the
+ * phase difference between the VCO and the PLL's currently
+ * selected reference; it cannot be pointed at an arbitrary input.
+ * For any input that is not the active reference there is no
+ * meaningful per-pin phase offset, so report 0 instead of the
+ * active reference's value.
+ */
+ if (sitdev->chan[sitdpll->id].selected_ref != dpin->id) {
+ mutex_unlock(&sitdev->multiop_lock);
+ dpin->phase_offset = 0;
+ *phase_offset = 0;
+ return 0;
+ }
+
+ rc = sit9531x_phase_offset_read(sitdev, sitdpll->id, &offset);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ /*
+ * -ENODEV means the PLL has no programmed DIVN (unused on this
+ * board); report phase_offset = 0 so a full pin-get dump does not
+ * fail just because one DPLL is dormant.
+ */
+ if (rc == -ENODEV) {
+ dpin->phase_offset = 0;
+ *phase_offset = 0;
+ return 0;
+ }
+ if (rc) {
+ NL_SET_ERR_MSG(extack, "TDC phase readback failed");
+ return rc;
+ }
+
+ /*
+ * The ABI reports phase offset in units of 1/DPLL_PHASE_OFFSET_DIVIDER
+ * picoseconds: the integer part of the attribute is the value divided
+ * by the divider, the remainder is the fraction. The TDC resolves one
+ * VCO period (hundreds of picoseconds), so the fractional digits are
+ * always zero here, but the magnitude still has to be scaled or every
+ * reading would be reported a thousand times too small.
+ */
+ offset *= DPLL_PHASE_OFFSET_DIVIDER;
+
+ dpin->phase_offset = offset;
+ *phase_offset = offset;
+ return 0;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
+ .direction_get = sit9531x_dpll_input_pin_direction_get,
+ .frequency_get = sit9531x_dpll_input_pin_frequency_get,
+ .state_on_dpll_get = sit9531x_dpll_input_pin_state_on_dpll_get,
+ .state_on_dpll_set = sit9531x_dpll_input_pin_state_on_dpll_set,
+ .prio_get = sit9531x_dpll_input_pin_prio_get,
+ .prio_set = sit9531x_dpll_input_pin_prio_set,
+ .phase_adjust_get = sit9531x_dpll_input_pin_phase_adjust_get,
+ .phase_offset_get = sit9531x_dpll_input_pin_phase_offset_get,
+};
+
+/* ====================================================================
+ * INTSYNC pin ops
+ *
+ * INTSYNC is the chip's inter-PLL sync net: one PLL drives it and other
+ * PLLs may lock to it instead of to an external reference. The two
+ * roles are exposed as two separate pins so neither overloads the other:
+ *
+ * - a source (output) pin registered on every DPLL. Connecting it on a
+ * DPLL makes that DPLL drive INTSYNC; only one DPLL may drive it at a
+ * time. It has no priority ops -- driving the net is not a reference
+ * selection.
+ * - a destination (input) pin registered on every DPLL. Connecting it
+ * on a DPLL makes that DPLL eligible to lock to INTSYNC as a
+ * reference, so it carries the priority ops.
+ * ====================================================================
+ */
+
+/* ---- INTSYNC source (output) pin ---- */
+
+/* The INTSYNC source pin is an output; its direction_get is defined below. */
+static int
+sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_direction *direction,
+ struct netlink_ext_ack *extack);
+
+static int
+sit9531x_dpll_intsync_src_state_on_dpll_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state *state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+
+ mutex_lock(&sitdev->multiop_lock);
+ if (sitdev->intsync_src == sitdpll->id)
+ *state = DPLL_PIN_STATE_CONNECTED;
+ else
+ *state = DPLL_PIN_STATE_DISCONNECTED;
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_intsync_src_state_on_dpll_set - drive INTSYNC from a PLL
+ *
+ * CONNECTED -> this PLL drives the INTSYNC net
+ * DISCONNECTED -> stop driving INTSYNC if this PLL drives it
+ *
+ * SELECTABLE is rejected: driving the net is an explicit output routing,
+ * not an automatic-selection candidate, matching the regular output pin.
+ */
+static int
+sit9531x_dpll_intsync_src_state_on_dpll_set(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ int rc = 0;
+
+ 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;
+ }
+ rc = sit9531x_intsync_enable(sitdev, sitdpll->id);
+ if (!rc)
+ sitdev->intsync_src = sitdpll->id;
+ break;
+ case DPLL_PIN_STATE_DISCONNECTED:
+ if (sitdev->intsync_src != sitdpll->id)
+ break;
+ rc = sit9531x_intsync_disable(sitdev, sitdpll->id);
+ if (!rc)
+ sitdev->intsync_src = -1;
+ break;
+ default:
+ rc = -EINVAL;
+ break;
+ }
+
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc && rc != -EBUSY)
+ NL_SET_ERR_MSG(extack, "Failed to set INTSYNC source state");
+
+ return rc;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_intsync_src_pin_ops = {
+ .direction_get = sit9531x_dpll_output_pin_direction_get,
+ .state_on_dpll_get = sit9531x_dpll_intsync_src_state_on_dpll_get,
+ .state_on_dpll_set = sit9531x_dpll_intsync_src_state_on_dpll_set,
+};
+
+/* ---- INTSYNC destination (input) pin ---- */
+
+/*
+ * sit9531x_dpll_intsync_dst_state_on_dpll_get - INTSYNC reference state
+ *
+ * CONNECTED when this PLL is locked to INTSYNC as its reference,
+ * SELECTABLE when it runs in automatic mode and a source PLL is driving
+ * INTSYNC, DISCONNECTED otherwise. The PLL that drives INTSYNC can never
+ * be its own destination.
+ */
+static int
+sit9531x_dpll_intsync_dst_state_on_dpll_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state *state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ const struct sit9531x_chan *chan;
+
+ chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
+
+ mutex_lock(&sitdev->multiop_lock);
+ if (sitdev->intsync_src == sitdpll->id)
+ *state = DPLL_PIN_STATE_DISCONNECTED;
+ else if (chan->locked && !chan->inner_lol &&
+ chan->selected_ref == SIT9531X_INTSYNC_PIN_ID)
+ *state = DPLL_PIN_STATE_CONNECTED;
+ else if (!chan->mode && sitdev->intsync_src >= 0)
+ *state = DPLL_PIN_STATE_SELECTABLE;
+ else
+ *state = DPLL_PIN_STATE_DISCONNECTED;
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_intsync_dst_state_on_dpll_set - lock a PLL to INTSYNC
+ *
+ * CONNECTED/SELECTABLE -> add INTSYNC to this PLL's priority table
+ * DISCONNECTED -> drop INTSYNC from this PLL's priority table
+ *
+ * INTSYNC is an internal net with no physical receiver, so only the
+ * per-PLL priority table is touched; the source pin controls generation.
+ */
+static int
+sit9531x_dpll_intsync_dst_state_on_dpll_set(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ u8 hw_src = sit9531x_input_hw_src(SIT9531X_INTSYNC_PIN_ID);
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ switch (state) {
+ case DPLL_PIN_STATE_DISCONNECTED:
+ rc = sit9531x_input_prio_remove(sitdev, sitdpll->id, hw_src);
+ break;
+ case DPLL_PIN_STATE_SELECTABLE:
+ case DPLL_PIN_STATE_CONNECTED:
+ if (sitdev->intsync_src == sitdpll->id) {
+ NL_SET_ERR_MSG(extack,
+ "PLL cannot lock to the INTSYNC it drives");
+ rc = -EINVAL;
+ break;
+ }
+ rc = sit9531x_input_prio_add(sitdev, sitdpll->id, hw_src);
+ break;
+ default:
+ rc = -EINVAL;
+ break;
+ }
+
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc && rc != -EINVAL)
+ NL_SET_ERR_MSG(extack, "Failed to set INTSYNC input state");
+
+ return rc;
+}
+
+/*
+ * Do not add .frequency_get / the generic input state getter here: the
+ * destination pin id is SIT9531X_INTSYNC_PIN_ID, one past the end of the
+ * ref[] array (INTSYNC is an internal net with no ref[] entry). The ops
+ * below only ever key on chan[] and the priority table, never ref[id].
+ */
+static const struct dpll_pin_ops sit9531x_dpll_intsync_dst_pin_ops = {
+ .direction_get = sit9531x_dpll_input_pin_direction_get,
+ .state_on_dpll_get = sit9531x_dpll_intsync_dst_state_on_dpll_get,
+ .state_on_dpll_set = sit9531x_dpll_intsync_dst_state_on_dpll_set,
+ .prio_get = sit9531x_dpll_input_pin_prio_get,
+ .prio_set = sit9531x_dpll_input_pin_prio_set,
+};
+
+/* ====================================================================
+ * XO (crystal oscillator) pin ops
+ *
+ * The XO is the chip's internal reference oscillator that feeds every
+ * PLL. It is exposed so userspace can see the on-chip reference, but it
+ * cannot be routed or disconnected, so it is reported permanently
+ * connected and offers no state_on_dpll_set / prio ops.
+ * ====================================================================
+ */
+
+static int
+sit9531x_dpll_xo_pin_state_on_dpll_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state *state,
+ struct netlink_ext_ack *extack)
+{
+ *state = DPLL_PIN_STATE_CONNECTED;
+ return 0;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_xo_pin_ops = {
+ .direction_get = sit9531x_dpll_input_pin_direction_get,
+ .frequency_get = sit9531x_dpll_input_pin_frequency_get,
+ .state_on_dpll_get = sit9531x_dpll_xo_pin_state_on_dpll_get,
+};
+
+/* ====================================================================
+ * Output pin ops
+ * ====================================================================
+ */
+
+static int
+sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_direction *direction,
+ struct netlink_ext_ack *extack)
+{
+ *direction = DPLL_PIN_DIRECTION_OUTPUT;
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_output_pin_frequency_get - read output pin frequency
+ *
+ * Reads the DIVO divider back from the chip and computes the live
+ * frequency as Fvco / DIVO. Falls back to the cached value when the
+ * output is not resolvable through the divider chain (e.g. not mapped
+ * to a PLL), so a netlink dump never fails on such pins.
+ */
+static int
+sit9531x_dpll_output_pin_frequency_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, u64 *frequency,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_output_freq_get(sitdev, dpin->id, frequency);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc)
+ *frequency = sit9531x_out_state_get(sitdev, dpin->id)->freq;
+
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_output_pin_frequency_set - set output pin frequency
+ *
+ * computes DIVO = Fvco / frequency and writes the
+ * 34-bit output divider to the output system registers via
+ * sit9531x_output_freq_set().
+ */
+static int
+sit9531x_dpll_output_pin_frequency_set(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, u64 frequency,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ u8 actual_pll;
+ int rc;
+
+ /* Use the actual PLL that drives this output (populated by
+ * out_state_fetch from the chip's OUT_MAP registers), not the
+ * DPLL device id -- in our current registration all outputs are
+ * bound to DPLL 0 for convenience, but physically they may be
+ * driven by PLL A/B/C/D with different Fvco.
+ */
+ actual_pll = sitdev->out[dpin->id].pll_idx;
+
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll,
+ frequency);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc)
+ NL_SET_ERR_MSG(extack, "Output frequency set failed");
+
+ return rc;
+}
+
+/*
+ * sit9531x_dpll_output_pin_state_on_dpll_get - get output pin state
+ *
+ * reports CONNECTED when the output is driven and
+ * DISCONNECTED when it has been muted via sit9531x_output_disable().
+ */
+static int
+sit9531x_dpll_output_pin_state_on_dpll_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state *state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ const struct sit9531x_out *out;
+
+ out = sit9531x_out_state_get(sitdpll->dev, dpin->id);
+ *state = out->enabled ? DPLL_PIN_STATE_CONNECTED
+ : DPLL_PIN_STATE_DISCONNECTED;
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_output_pin_state_on_dpll_set - mute/un-mute an output
+ *
+ * forces Hi-Z on the output pin via the Page 0x03
+ * force/state register pair.
+ * CONNECTED -> enable (release force, back to factory default)
+ * DISCONNECTED -> disable (force Hi-Z)
+ */
+static int
+sit9531x_dpll_output_pin_state_on_dpll_set(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ switch (state) {
+ case DPLL_PIN_STATE_CONNECTED:
+ rc = sit9531x_output_enable(sitdev, dpin->id);
+ break;
+ case DPLL_PIN_STATE_DISCONNECTED:
+ rc = sit9531x_output_disable(sitdev, dpin->id);
+ break;
+ default:
+ rc = -EINVAL;
+ break;
+ }
+
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc)
+ NL_SET_ERR_MSG(extack, "Failed to set output pin state");
+
+ return rc;
+}
+
+/*
+ * sit9531x_dpll_output_pin_phase_adjust_get - read output phase adjustment
+ *
+ * returns cached value.
+ */
+static int
+sit9531x_dpll_output_pin_phase_adjust_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, s32 *phase_adjust,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+
+ *phase_adjust = dpin->phase_adjust;
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_output_pin_phase_adjust_set - set output phase adjustment
+ *
+ * Programs the per-output PRG_RST_DELAY registers for deterministic
+ * phase offset; see sit9531x_output_phase_adjust_set() in core.c.
+ */
+static int
+sit9531x_dpll_output_pin_phase_adjust_set(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, s32 phase_adjust,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_output_phase_adjust_set(sitdev, dpin->id, phase_adjust);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc) {
+ NL_SET_ERR_MSG(extack, "Phase adjust failed");
+ return rc;
+ }
+
+ dpin->phase_adjust = phase_adjust;
+ return 0;
+}
+
+static int
+sit9531x_dpll_output_pin_esync_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ struct dpll_pin_esync *esync,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+
+ if (!sit9531x_dpll_esync_pin_supported(dpin))
+ return -EOPNOTSUPP;
+
+ esync->range = sit9531x_esync_ranges;
+ esync->range_num = ARRAY_SIZE(sit9531x_esync_ranges);
+ esync->pulse = SIT9531X_ESYNC_PULSE_DEFAULT;
+ esync->freq = dpin->esync_freq;
+
+ return 0;
+}
+
+static int
+sit9531x_dpll_output_pin_esync_set(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ u64 freq,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ u8 actual_pll;
+ int rc;
+
+ if (!sit9531x_dpll_esync_pin_supported(dpin)) {
+ NL_SET_ERR_MSG(extack,
+ "Embedded sync not enabled for this pin");
+ return -EOPNOTSUPP;
+ }
+
+ actual_pll = sitdev->out[dpin->id].pll_idx;
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ /*
+ * This output is a dedicated embedded-sync pin.
+ * Treat freq=0 as a request to disable the entire output.
+ */
+ if (!freq) {
+ rc = sit9531x_output_disable(sitdev, dpin->id);
+ if (!rc)
+ dpin->esync_freq = 0;
+ mutex_unlock(&sitdev->multiop_lock);
+ return rc;
+ }
+
+ if (freq != SIT9531X_ESYNC_FREQ_10MHZ) {
+ mutex_unlock(&sitdev->multiop_lock);
+ NL_SET_ERR_MSG(extack,
+ "Only 10 MHz esync frequency is supported");
+ return -EINVAL;
+ }
+
+ rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll,
+ SIT9531X_ESYNC_FREQ_10MHZ);
+ /*
+ * Program the pulse generator (PROG0 PULSE_CTRL) so the embedded-sync
+ * pulse is actually emitted; without it the output carries the clock
+ * but no esync marker. SIT9531X_ESYNC_PULSE_DEFAULT is the same duty
+ * the esync_get callback advertises.
+ */
+ if (!rc)
+ rc = sit9531x_output_pulse_ctrl_set(sitdev, dpin->id,
+ SIT9531X_ESYNC_PULSE_DEFAULT);
+ if (!rc)
+ rc = sit9531x_output_enable(sitdev, dpin->id);
+
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (!rc)
+ dpin->esync_freq = SIT9531X_ESYNC_FREQ_10MHZ;
+
+ return rc;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_output_pin_ops = {
+ .direction_get = sit9531x_dpll_output_pin_direction_get,
+ .frequency_get = sit9531x_dpll_output_pin_frequency_get,
+ .frequency_set = sit9531x_dpll_output_pin_frequency_set,
+ .state_on_dpll_get = sit9531x_dpll_output_pin_state_on_dpll_get,
+ .state_on_dpll_set = sit9531x_dpll_output_pin_state_on_dpll_set,
+ .phase_adjust_get = sit9531x_dpll_output_pin_phase_adjust_get,
+ .phase_adjust_set = sit9531x_dpll_output_pin_phase_adjust_set,
+ .esync_get = sit9531x_dpll_output_pin_esync_get,
+ .esync_set = sit9531x_dpll_output_pin_esync_set,
+};
+
+/* ====================================================================
+ * Pin allocation, registration, and cleanup
+ * ====================================================================
+ */
+
+static const struct dpll_pin_ops *
+sit9531x_dpll_pin_ops_get(const struct sit9531x_dpll_pin *pin)
+{
+ if (!sit9531x_dpll_is_input_pin(pin)) {
+ if (sit9531x_dpll_is_intsync_src_pin(pin))
+ return &sit9531x_dpll_intsync_src_pin_ops;
+ return &sit9531x_dpll_output_pin_ops;
+ }
+ if (sit9531x_dpll_is_intsync_pin(pin))
+ return &sit9531x_dpll_intsync_dst_pin_ops;
+ if (sit9531x_dpll_is_xo_pin(pin))
+ return &sit9531x_dpll_xo_pin_ops;
+ return &sit9531x_dpll_input_pin_ops;
+}
+
+/*
+ * sit9531x_dpll_pin_alloc - allocate a DPLL pin
+ * @sitdpll: DPLL device this pin belongs to
+ * @dir: pin direction
+ * @id: hardware pin index
+ *
+ * Return: pointer to allocated pin on success, error pointer on error
+ */
+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_free - deallocate a DPLL pin
+ * @pin: pin to free
+ */
+static void sit9531x_dpll_pin_free(struct sit9531x_dpll_pin *pin)
+{
+ WARN(pin->dpll_pin, "DPLL pin is still registered\n");
+ kfree(pin);
+}
+
+/*
+ * sit9531x_dpll_pin_register - register a DPLL pin with the subsystem
+ * @pin: pin to register
+ * @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.
+ *
+ * Return: 0 on success, <0 on error
+ */
+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;
+
+ /* Get pin properties from firmware nodes */
+ props = sit9531x_pin_props_get(sitdpll->dev, pin->dir, pin->id);
+ if (IS_ERR(props))
+ return PTR_ERR(props);
+
+ /* Save package label and firmware node */
+ strscpy(pin->label, props->package_label, sizeof(pin->label));
+ pin->fwnode = fwnode_handle_get(props->fwnode);
+ pin->esync_control = props->esync_control;
+
+ /* Create or get existing DPLL pin */
+ 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);
+
+ /* Register the pin */
+ rc = dpll_pin_register(sitdpll->dpll_dev, pin->dpll_pin, ops, pin);
+ if (rc)
+ goto err_register;
+
+ /* Free pin properties */
+ sit9531x_pin_props_put(props);
+
+ return 0;
+
+err_register:
+ dpll_pin_put(pin->dpll_pin, &pin->tracker);
+err_pin_get:
+ /*
+ * On the dpll_pin_get() failure path pin->dpll_pin holds an ERR_PTR;
+ * clear it so the caller's sit9531x_dpll_pin_free() does not mistake
+ * it for a still-registered pin and emit a spurious WARN().
+ */
+ pin->dpll_pin = NULL;
+ fwnode_handle_put(pin->fwnode);
+ pin->fwnode = NULL;
+ sit9531x_pin_props_put(props);
+
+ return rc;
+}
+
+/*
+ * sit9531x_dpll_pin_unregister - unregister a DPLL pin
+ * @pin: pin to unregister
+ */
+static void sit9531x_dpll_pin_unregister(struct sit9531x_dpll_pin *pin)
+{
+ struct sit9531x_dpll *sitdpll = pin->dpll;
+ const struct dpll_pin_ops *ops;
+
+ WARN(!pin->dpll_pin, "DPLL pin is not registered\n");
+
+ 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;
+}
+
+/*
+ * sit9531x_dpll_pins_unregister - unregister all pins on a DPLL
+ * @sitdpll: DPLL device
+ */
+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);
+ sit9531x_dpll_pin_free(pin);
+ }
+}
+
+/*
+ * sit9531x_input_pin_is_registrable - check if an input pin is registrable
+ * @sitdev: device pointer
+ * @index: input pin index
+ *
+ * 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
+ * @sitdpll: DPLL device
+ * @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
+ * @sitdpll: DPLL device
+ *
+ * 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.
+ *
+ * Return: 0 on success, <0 on error
+ */
+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) {
+ sit9531x_dpll_pin_free(pin);
+ goto error;
+ }
+
+ list_add(&pin->list, &sitdpll->pins);
+ }
+
+ return 0;
+
+error:
+ sit9531x_dpll_pins_unregister(sitdpll);
+ return rc;
+}
+
+/* ====================================================================
+ * DPLL device registration
+ * ====================================================================
+ */
+
+/*
+ * sit9531x_dpll_device_register - register a DPLL device
+ * @sitdpll: DPLL to register
+ *
+ * Return: 0 on success, <0 on error
+ */
+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;
+}
+
+/*
+ * sit9531x_dpll_device_unregister - unregister a DPLL device
+ * @sitdpll: DPLL to unregister
+ */
+static void sit9531x_dpll_device_unregister(struct sit9531x_dpll *sitdpll)
+{
+ WARN(!sitdpll->dpll_dev, "DPLL device is not registered\n");
+
+ dpll_device_unregister(sitdpll->dpll_dev, &sitdpll->ops, sitdpll);
+ dpll_device_put(sitdpll->dpll_dev, &sitdpll->tracker);
+ sitdpll->dpll_dev = NULL;
+}
+
+/* ====================================================================
+ * DPLL allocation and top-level register/unregister
+ * ====================================================================
+ */
+
+/**
+ * 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)
+{
+ WARN(sitdpll->dpll_dev, "DPLL device is still registered\n");
+ kfree(sitdpll);
+}
+
+/**
+ * sit9531x_dpll_register - register DPLL device and all its pins
+ * @sitdpll: DPLL device
+ *
+ * Registers the DPLL device with the subsystem and then registers
+ * all input and output pins that are connected to this PLL.
+ *
+ * Return: 0 on success, <0 on error
+ */
+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
+ * @sitdpll: DPLL device
+ */
+void sit9531x_dpll_unregister(struct sit9531x_dpll *sitdpll)
+{
+ sit9531x_dpll_pins_unregister(sitdpll);
+ sit9531x_dpll_device_unregister(sitdpll);
+}
+
+/* ====================================================================
+ * Periodic change detection
+ * ====================================================================
+ */
+
+/**
+ * sit9531x_dpll_changes_check - check for state changes and notify
+ * @sitdpll: DPLL device
+ *
+ * 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;
+
+ /* Get current lock status */
+ 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);
+ }
+
+ /* Check pins for autonomous state changes */
+ 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..76b3c5e2b95a
--- /dev/null
+++ b/drivers/dpll/sit9531x/dpll.h
@@ -0,0 +1,87 @@
+/* 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;
+
+/**
+ * struct sit9531x_dpll_pin - per-pin DPLL state
+ * @list: linked list entry within dpll->pins
+ * @dpll: back-pointer to owning DPLL
+ * @dpll_pin: registered dpll_pin object
+ * @tracker: reference count tracker for dpll_pin_get/put
+ * @fwnode: firmware node handle (from DT)
+ * @label: package label string (e.g. "IN0", "OUT3")
+ * @dir: pin direction (INPUT or OUTPUT)
+ * @id: hardware pin index (input 0-N or output 0-M)
+ * @prio: current priority for automatic input selection
+ * @pin_state: last saved pin state
+ * @phase_adjust: current phase adjustment in picoseconds
+ * @phase_offset: last measured phase offset
+ * @esync_control: esync/sysref control allowed for this output pin
+ * @esync_freq: last requested esync frequency (0 means disabled)
+ */
+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];
+ enum dpll_pin_direction dir;
+ u8 id;
+ u8 prio;
+ enum dpll_pin_state pin_state;
+ s32 phase_adjust;
+ s64 phase_offset;
+ bool esync_control;
+ u64 esync_freq;
+};
+
+/**
+ * struct sit9531x_dpll - per-PLL DPLL device state
+ * @list: linked list entry within sitdev->dplls
+ * @dev: back-pointer to parent sit9531x_dev
+ * @dpll_dev: registered dpll_device object
+ * @tracker: reference count tracker for dpll_device_get/put
+ * @ops: copy of dpll_device_ops (per-instance)
+ * @pins: list of registered pins
+ * @id: PLL channel number (0 = PLLA, 3 = PLLD)
+ * @lock_status: cached DPLL lock status
+ */
+struct sit9531x_dpll {
+ struct list_head list;
+ struct sit9531x_dev *dev;
+ struct dpll_device *dpll_dev;
+ dpll_tracker tracker;
+ struct dpll_device_ops ops;
+ struct list_head pins;
+ u8 id;
+ enum dpll_lock_status lock_status;
+};
+
+/* ---- DPLL allocation and registration ---- */
+struct sit9531x_dpll *sit9531x_dpll_alloc(struct sit9531x_dev *sitdev, u8 ch);
+void sit9531x_dpll_free(struct sit9531x_dpll *sitdpll);
+int sit9531x_dpll_register(struct sit9531x_dpll *sitdpll);
+void sit9531x_dpll_unregister(struct sit9531x_dpll *sitdpll);
+
+/* ---- Periodic change detection ---- */
+void sit9531x_dpll_changes_check(struct sit9531x_dpll *sitdpll);
+
+#endif /* _SIT9531X_DPLL_H */
diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
new file mode 100644
index 000000000000..6fafac92a16a
--- /dev/null
+++ b/drivers/dpll/sit9531x/prop.c
@@ -0,0 +1,415 @@
+// 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
+ * @sitdev: pointer to device
+ * @props: pointer to pin properties
+ * @id: input pin index
+ *
+ * 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
+ * @sitdev: pointer to device
+ * @props: pointer to pin properties
+ * @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
+ * @sitdev: pointer to device
+ * @props: pointer to pin properties
+ * @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";
+
+ /* Get node containing input or 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", ®))
+ continue;
+
+ if (id == reg)
+ break;
+ }
+
+ /* Release parent node */
+ fwnode_handle_put(pins_node);
+
+ /* Save found 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
+ * @sitdev: pointer to device
+ * @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);
+
+ /* Set default pin type and capabilities */
+ if (dir == DPLL_PIN_DIRECTION_INPUT &&
+ index == SIT9531X_INTSYNC_PIN_ID) {
+ /*
+ * INTSYNC destination pin: a PLL locks to the INTSYNC net as a
+ * reference, so it can be connected and re-prioritised.
+ */
+ props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+ props->dpll_props.capabilities =
+ DPLL_PIN_CAPABILITIES_PRIORITY_CAN_CHANGE |
+ DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+ curr_freq = 0;
+ } else if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+ index == SIT9531X_INTSYNC_OUT_PIN_ID) {
+ /*
+ * INTSYNC source pin: a PLL drives the INTSYNC net. It can be
+ * connected/disconnected but carries no priority (driving the
+ * net is not a reference selection) and no frequency.
+ */
+ props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+ props->dpll_props.capabilities =
+ DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+ curr_freq = 0;
+ } else if (dir == DPLL_PIN_DIRECTION_INPUT &&
+ index == SIT9531X_MAX_INPUTS) {
+ /* The XO reference is fixed: no state or priority control. */
+ props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+ props->dpll_props.capabilities = 0;
+ sitdev->ref[index].freq = sitdev->xtal_freq;
+ curr_freq = sitdev->xtal_freq;
+ } else if (dir == DPLL_PIN_DIRECTION_INPUT) {
+ props->dpll_props.type = DPLL_PIN_TYPE_EXT;
+ props->dpll_props.capabilities =
+ DPLL_PIN_CAPABILITIES_PRIORITY_CAN_CHANGE |
+ DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+ curr_freq = sitdev->ref[index].freq;
+ } else {
+ props->dpll_props.type = DPLL_PIN_TYPE_GNSS;
+ props->dpll_props.capabilities =
+ DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+ curr_freq = sitdev->out[index].freq;
+ }
+
+ /* Allow phase-adjust over +/-1 ms window. The subsystem rejects
+ * pin_set(phase-adjust, X) when X falls outside [min, max], so
+ * leaving these at 0 silently blocks every netlink call. 1 ms is
+ * well beyond the DCO dynamic range but costs nothing.
+ */
+ props->dpll_props.phase_range.min = -1000000000; /* -1 ms in ps */
+ props->dpll_props.phase_range.max = 1000000000; /* +1 ms in ps */
+
+ /* Generate package label */
+ sit9531x_prop_pin_package_label_set(sitdev, props, dir, index);
+
+ /*
+ * Both INTSYNC pins are internal to the chip and have no board-level
+ * wiring, so they take no properties from the firmware node.
+ */
+ if (dir == DPLL_PIN_DIRECTION_INPUT &&
+ index == SIT9531X_INTSYNC_PIN_ID)
+ goto skip_fwnode_props;
+ if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+ index == SIT9531X_INTSYNC_OUT_PIN_ID)
+ goto skip_fwnode_props;
+
+ /* Get firmware node for the given pin */
+ 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);
+ }
+
+ /* Check for embedded sync control */
+ props->esync_control =
+ fwnode_property_read_bool(props->fwnode, "esync-control");
+
+ /* Read supported frequencies property */
+ num_freqs = fwnode_property_count_u64(props->fwnode,
+ "supported-frequencies-hz");
+ if (num_freqs <= 0) {
+ num_freqs = 0;
+ goto skip_fwnode_props;
+ }
+
+ /* Read frequency list into temporary array */
+ 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;
+
+ /* Copy DT-listed freqs (if any), skipping curr_freq */
+ for (i = 0; i < num_freqs; i++) {
+ struct dpll_pin_frequency freq = DPLL_PIN_FREQUENCY(freqs[i]);
+
+ if (freqs[i] == curr_freq)
+ continue;
+ ranges[j++] = freq;
+ }
+
+ /* 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
+ * @sitdev: pointer to device
+ * @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_info(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..d0f5cccb435c
--- /dev/null
+++ b/drivers/dpll/sit9531x/prop.h
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * SiTime SiT9531x firmware node property parsing
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ */
+
+#ifndef _SIT9531X_PROP_H
+#define _SIT9531X_PROP_H
+
+#include <linux/dpll.h>
+#include <linux/fwnode.h>
+
+struct sit9531x_dev;
+
+/**
+ * struct sit9531x_pin_props - pin properties from firmware
+ * @fwnode: firmware node handle (NULL if no DT node)
+ * @dpll_props: DPLL core pin properties
+ * @package_label: pin package label (e.g. "IN0", "OUT3")
+ * @esync_control: embedded sync is controllable
+ */
+struct sit9531x_pin_props {
+ struct fwnode_handle *fwnode;
+ struct dpll_pin_properties dpll_props;
+ char package_label[8];
+ bool esync_control;
+};
+
+enum dpll_type sit9531x_prop_dpll_type_get(struct sit9531x_dev *sitdev,
+ u8 index);
+struct sit9531x_pin_props *sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
+ enum dpll_pin_direction dir,
+ u8 index);
+void sit9531x_pin_props_put(struct sit9531x_pin_props *props);
+
+#endif /* _SIT9531X_PROP_H */
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
new file mode 100644
index 000000000000..3c527ca4aaf6
--- /dev/null
+++ b/drivers/dpll/sit9531x/regs.h
@@ -0,0 +1,419 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * SiTime SiT9531x register definitions
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ */
+
+#ifndef _SIT9531X_REGS_H
+#define _SIT9531X_REGS_H
+
+/*
+ * I2C register model:
+ * - Page select register at offset 0x01
+ * - Each page has 128 registers (0x00-0x7F)
+ * - Some pages are paired (e.g. 0x0A/0x1A for PLLA)
+ */
+#define SIT9531X_PAGE_SEL 0xFF
+#define SIT9531X_PAGE_SIZE 0x100
+#define SIT9531X_NUM_PAGES 32
+
+/* Helper macros for page:offset addressing */
+#define SIT9531X_REG(_page, _offset) (((_page) << 8) | (_offset))
+#define SIT9531X_REG_PAGE(_reg) ((_reg) >> 8)
+#define SIT9531X_REG_OFFSET(_reg) ((_reg) & 0xFF)
+
+/* ---- Page definitions ---- */
+#define SIT9531X_PAGE_MAINSYS0 0x00
+#define SIT9531X_PAGE_MAINSYS1 0x01
+#define SIT9531X_PAGE_INPUTSYS 0x02
+#define SIT9531X_PAGE_OUTSYS0 0x03
+#define SIT9531X_PAGE_OUTSYS1 0x04
+#define SIT9531X_PAGE_CLKMON0 0x06
+#define SIT9531X_PAGE_CLKMON1 0x07
+#define SIT9531X_PAGE_PLLA 0x0A
+#define SIT9531X_PAGE_PLLA_EXT 0x1A
+#define SIT9531X_PAGE_PLLB 0x0B
+#define SIT9531X_PAGE_PLLB_EXT 0x1B
+#define SIT9531X_PAGE_PLLC 0x0C
+#define SIT9531X_PAGE_PLLC_EXT 0x1C
+#define SIT9531X_PAGE_PLLD 0x0D
+#define SIT9531X_PAGE_PLLD_EXT 0x1D
+
+/* PLL index to page mapping */
+#define SIT9531X_PLL_PAGE(_idx) \
+ (SIT9531X_PAGE_PLLA + (_idx))
+
+/* ---- Page 0x00 (Main System) registers ---- */
+/*
+ * 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)
+#define SIT9531X_REG_HOLDOVER_STATUS SIT9531X_REG(0x00, 0x06)
+#define SIT9531X_REG_SYNC_STATUS SIT9531X_REG(0x00, 0x07)
+#define SIT9531X_REG_STATUS_1 SIT9531X_REG(0x00, 0x06)
+#define SIT9531X_REG_STATUS_2 SIT9531X_REG(0x00, 0x0A)
+
+/* DCO trigger register (Page 0x00) */
+#define SIT9531X_REG_DCO_TRIGGER SIT9531X_REG(0x00, 0x64)
+#define SIT9531X_DCO_TRIGGER_INCR BIT(6)
+#define SIT9531X_DCO_TRIGGER_DECR BIT(4)
+#define SIT9531X_DCO_TRIGGER_BASE 0xAE
+
+/* DCO trigger pulse timing: minimum 6 ns required by hardware */
+#define SIT9531X_DCO_TRIGGER_PULSE_NS 167
+
+#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
+
+/* ---- Page 0x01 (Input Priority Table) registers ---- */
+/*
+ * Priority table: 6 registers per PLL, each holds two priority slots
+ * nibble-packed (even slot in [3:0], odd slot in [7:4]).
+ *
+ * Base registers for PLLA: 0x16-0x1B (slots 0-11).
+ * 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
+#define SIT9531X_PRIO_MAX_SLOTS 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 -- Global update register (PRG_CMD / NVM / loop lock) */
+#define SIT9531X_REG_GLOBAL_UPDATE SIT9531X_REG(0x00, 0x0F)
+
+/* PLL holdover control (PLL page offset) */
+#define SIT9531X_PLL_REG_HO_CTRL 0x6F
+#define SIT9531X_PLL_HO_FORCE_BIT 4
+
+/* ---- Page 0x02 (Input System) -- input disable control ---- */
+/* 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)
+
+/* ---- Page 0x02 -- CLKINx_INPUT_MODE (per input pair) ---- */
+/*
+ * 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_811_OE_MASK SIT9531X_REG(0x03, 0xF4)
+#define SIT9531X_REG_HIZ_SE_811_OE_STATE SIT9531X_REG(0x03, 0xF5)
+#define SIT9531X_REG_HIZ_SE_07_OE_MASK SIT9531X_REG(0x03, 0xF6)
+#define SIT9531X_REG_HIZ_SE_07_OE_STATE SIT9531X_REG(0x03, 0xF7)
+#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)
+
+/* ---- Pages 0x03/0x04 -- Output divider (DIVO) registers ---- */
+/*
+ * 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
+
+/* ---- Pages 0x03/0x04 -- Output PRG_RST_DELAY (per-output phase delay) ---- */
+/*
+ * 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
+
+/* ---- Pages 0x03/0x04 -- Output PROG0 (PULSE_CTRL, 8-bit) ---- */
+/*
+ * 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
+
+/* ---- Page 0 -- per-PLL DIVO trigger enables (NVMSPARE1_GENERIC, 0x19) ---- */
+/*
+ * One bit per PLL (A=0, B=1, C=2, D=3) for the small-change (SYSREF
+ * trigger) path; bit n+4 enables the large-change (DIVO restart)
+ * path for the same PLL. See SiT95316 register map p.6.
+ */
+#define SIT9531X_REG_DIVO_TRIGGER_EN SIT9531X_REG(0x00, 0x19)
+#define SIT9531X_DIVO_SYSREF_TRIG_BIT(_pll) (_pll)
+#define SIT9531X_DIVO_LARGE_TRIG_BIT(_pll) ((_pll) + 4)
+
+/* ---- Page 0 -- GPIO_INPUT_FUNC_CTRL1 (0x65): register phase trigger ---- */
+/*
+ * 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_CONFIG47_PLL (per PLL page reg 0x47) ---- */
+/*
+ * Mode bits 6/5/4 select the SYSREF/SYNCB/PULSER variants; bits 3:0
+ * carry DIVO_SYS_REF[11:8] of the 12-bit one-hot output select that
+ * continues into reg 0x48[7:0]. See register map p.84.
+ */
+#define SIT9531X_PLL_REG_SYSREF_MODE 0x47
+#define SIT9531X_PLL_SYSREF_PULSER_BIT BIT(6)
+#define SIT9531X_PLL_SYSREF_MODE_BIT BIT(5)
+#define SIT9531X_PLL_SYSREF_SYNCB_BIT BIT(4)
+#define SIT9531X_PLL_SYSREF_MODE_MASK (SIT9531X_PLL_SYSREF_PULSER_BIT | \
+ SIT9531X_PLL_SYSREF_MODE_BIT | \
+ SIT9531X_PLL_SYSREF_SYNCB_BIT)
+#define SIT9531X_PLL_SYSREF_TARGET_HI_MASK 0x0F
+#define SIT9531X_PLL_REG_SYSREF_SEL 0x48
+
+/*
+ * SYSREF output-buffer enable. Per-PLL-page registers 0xD5/0xD6 bit 7
+ * gate the SYSREF/pulser output buffer and must be set in addition to the
+ * mode bits in 0x47; with the buffer disabled the mode/target/trigger are
+ * programmed but the pad stays silent. These are not in the public
+ * register map (debug registers); the enable sequence is from the vendor.
+ */
+#define SIT9531X_PLL_REG_SYSREF_OE0 0xD5
+#define SIT9531X_PLL_REG_SYSREF_OE1 0xD6
+#define SIT9531X_PLL_SYSREF_OE_BIT BIT(7)
+
+/* ---- PLL page registers (apply to pages 0x0A-0x0D) ---- */
+#define SIT9531X_PLL_REG_SMALL_UPDATE 0x0F
+
+/* On-demand phase-flush enable (PLL page reg 0x3D bit 7) */
+#define SIT9531X_PLL_REG_PHFL_CTRL 0x3D
+#define SIT9531X_PLL_PHFL_ON_DEMAND_EN BIT(7)
+
+/*
+ * Loop-filter coefficients on PLL_PAGE regs 0x10-0x15 (3 normal +
+ * 3 fast-lock) are GUI/NVM-generated by the timing configurator and must not be
+ * reprogrammed at runtime; the register map flags them as
+ * "GUI generated configuration should not change manually".
+ */
+
+#define SIT9531X_PLL_REG_OUT_MAP_HI 0x27
+#define SIT9531X_PLL_REG_OUT_MAP_LO 0x28
+#define SIT9531X_PLL_REG_STATUS 0x31
+#define SIT9531X_PLL_REG_NVM_UPDATE 0x3F
+
+/* DIVN registers (free-run divider readback) */
+#define SIT9531X_PLL_REG_DIVN_INT 0x30
+#define SIT9531X_PLL_REG_DIVN_NUM 0x32 /* 4 bytes (0x32-0x35) */
+#define SIT9531X_PLL_REG_DIVN_DEN 0x38 /* 4 bytes (0x38-0x3B) */
+
+/* DIVN2 registers (sync divider readback) */
+#define SIT9531X_PLL_REG_DIVN2_INT 0x3E /* 5 bytes (0x3E-0x42) */
+#define SIT9531X_PLL_REG_DIVN2_FRAC_NUM 0x43 /* 4 bytes (0x43-0x46) */
+#define SIT9531X_PLL_REG_DIVN2_FRAC_DEN 0x49 /* 4 bytes (0x49-0x4C) */
+
+/* Inner loop DCO word registers (48-bit fractional) */
+#define SIT9531X_PLL_REG_DCO_FRAC1 0x51
+#define SIT9531X_PLL_REG_DCO_FRAC2 0x52
+#define SIT9531X_PLL_REG_DCO_FRAC3 0x53
+#define SIT9531X_PLL_REG_DCO_FRAC4 0x54
+#define SIT9531X_PLL_REG_DCO_FRAC5 0x55
+#define SIT9531X_PLL_REG_DCO_FRAC6 0x56
+
+/* DCO function register */
+#define SIT9531X_PLL_REG_DCO_FUNC 0x57
+#define SIT9531X_DCO_MASK BIT(0)
+#define SIT9531X_DCO_EN BIT(1)
+#define SIT9531X_DCO_OUTER_EN BIT(4)
+#define SIT9531X_DCO_DITHER_MODE BIT(6)
+
+/* Outer loop DCO integer registers (24-bit) */
+#define SIT9531X_PLL_REG_DCO_INT_7 0x5E
+#define SIT9531X_PLL_REG_DCO_INT_15 0x5F
+#define SIT9531X_PLL_REG_DCO_INT_23 0x60
+
+/* Outer loop DCO fractional registers (32-bit, shifted <<16) */
+#define SIT9531X_PLL_REG_DCO_OFRAC_7 0x63
+#define SIT9531X_PLL_REG_DCO_OFRAC_15 0x64
+#define SIT9531X_PLL_REG_DCO_OFRAC_23 0x65
+#define SIT9531X_PLL_REG_DCO_OFRAC_31 0x66
+
+/* Debug register unlock */
+#define SIT9531X_PLL_REG_DEBUG 0xBD
+#define SIT9531X_PLL_DEBUG_UNLOCK 0xC3
+
+/* TDC (Time-to-Digital Converter) phase measurement -- PLL page */
+#define SIT9531X_PLL_REG_TDC_CFG 0xB3
+#define SIT9531X_PLL_REG_TDC_MODE 0xB4
+#define SIT9531X_TDC_MODE_ENABLE 0x80
+#define SIT9531X_TDC_CFG_DEFAULT 69
+#define SIT9531X_PLL_REG_TDC_DATA_0 0xB5 /* [7:0] */
+#define SIT9531X_PLL_REG_TDC_DATA_1 0xB6 /* [15:8] */
+#define SIT9531X_PLL_REG_TDC_DATA_2 0xB7 /* [23:16] */
+#define SIT9531X_PLL_REG_TDC_DATA_3 0xB8 /* [31:24] */
+#define SIT9531X_PLL_REG_TDC_DATA_4 0xB9 /* [39:32] + sign */
+#define SIT9531X_TDC_SIGN_BIT 3
+#define SIT9531X_PLL_REG_TDC_TRIGGER 0xD0 /* read to latch TDC sample */
+
+/*
+ * TDC phase-difference measurement setup (PLL page). Per the vendor
+ * PhaseDifferenceReader reference procedure, a valid TDC reading needs
+ * the digital loop filter frozen (so the VCO does not track out the
+ * measured offset mid-sample) and, on PLLA (the 1PPS phase DPLL), the
+ * automatic phase/frequency-lock helpers held off.
+ */
+#define SIT9531X_PLL_REG_DLPF_FREEZE0 0x8A
+#define SIT9531X_PLL_REG_DLPF_FREEZE1 0x8B
+#define SIT9531X_PLL_DLPF_FREEZE_BIT BIT(5) /* freeze DLPF (hitless HO) */
+#define SIT9531X_PLL_REG_APL_CTRL 0x4D
+#define SIT9531X_PLL_APL_WAKEUP_DIS_BIT BIT(7) /* disable APL at wake-up */
+#define SIT9531X_PLL_APL_SWITCH_EN_BIT BIT(5) /* APL on switchover */
+#define SIT9531X_PLL_REG_AFL_CTRL 0x4E
+#define SIT9531X_PLL_AFL_WAKEUP_DIS_BIT BIT(5) /* disable AFL at wake-up */
+#define SIT9531X_PLL_CONFIG47_PHFL_BIT BIT(7) /* PHFL enable (reg 0x47) */
+#define SIT9531X_PLL_REG_ACTIVE 0x02
+#define SIT9531X_PLL_ACTIVE_BIT BIT(0) /* PLL reached active state */
+#define SIT9531X_PLL_REG_RESTART 0x05
+#define SIT9531X_PLL_RESTART_BIT BIT(0) /* restart the PLL */
+#define SIT9531X_PLL_REG_ZDB0 0x2B
+#define SIT9531X_PLL_REG_ZDB1 0x1E
+#define SIT9531X_PLL_ZDB_EN_BIT BIT(4) /* zero-delay buffer enabled */
+
+/* PLL EXT page INTSYNC configuration registers */
+#define SIT9531X_PLL_EXT_PAGE(_idx) (SIT9531X_PAGE_PLLA_EXT + (_idx))
+
+/* PLL STATUS register bits */
+#define SIT9531X_PLL_STATUS_LOCK BIT(0)
+#define SIT9531X_PLL_STATUS_OUTER_DIS BIT(5)
+
+/* Small update command */
+#define SIT9531X_PLL_SMALL_UPDATE_CMD 0x02
+
+/* ---- Clock monitor registers (Page 0x06) ---- */
+/*
+ * Per-input clock monitor status registers. Each register holds
+ * status for two inputs (even input in bits [3:0], odd in [7:4]).
+ * P-polarity and N-polarity inputs have separate register banks.
+ *
+ * Bit layout per input nibble:
+ * [0] freq_fine_drifted -- fine frequency drift detected
+ * [1] freq_coarse_drifted -- coarse frequency drift detected
+ * [2] clk_loss -- clock input loss (LOS)
+ * [3] clk_loss_fd -- clock input loss with freq drift
+ *
+ * Status register is at base offset, notification at base+1.
+ */
+
+/* P-polarity status registers */
+#define SIT9531X_CLKMON_P_STATUS_01 SIT9531X_REG(0x06, 0x02) /* inputs 0,1 */
+#define SIT9531X_CLKMON_P_NOTIF_01 SIT9531X_REG(0x06, 0x03)
+#define SIT9531X_CLKMON_P_STATUS_23 SIT9531X_REG(0x06, 0x06) /* inputs 2,3 */
+#define SIT9531X_CLKMON_P_NOTIF_23 SIT9531X_REG(0x06, 0x07)
+
+/* N-polarity status registers */
+#define SIT9531X_CLKMON_N_STATUS_01 SIT9531X_REG(0x06, 0x92) /* inputs 0,1 */
+#define SIT9531X_CLKMON_N_NOTIF_01 SIT9531X_REG(0x06, 0x93)
+#define SIT9531X_CLKMON_N_STATUS_23 SIT9531X_REG(0x06, 0x96) /* inputs 2,3 */
+#define SIT9531X_CLKMON_N_NOTIF_23 SIT9531X_REG(0x06, 0x97)
+
+/* Per-input bit offsets within clock monitor nibble */
+#define SIT9531X_CLKMON_FREQ_FINE 0 /* bit 0 / bit 4 */
+#define SIT9531X_CLKMON_FREQ_COARSE 1 /* bit 1 / bit 5 */
+#define SIT9531X_CLKMON_CLK_LOSS 2 /* bit 2 / bit 6 */
+#define SIT9531X_CLKMON_CLK_LOSS_FD 3 /* bit 3 / bit 7 */
+
+/* ---- Debug / NVM unlock registers ---- */
+#define SIT9531X_REG_DBG_UNLOCK1 0x24
+#define SIT9531X_REG_DBG_UNLOCK2 0x25
+
+/* ---- Variant ID values (single byte read from SIT9531X_REG_VARIANT_ID) ---- */
+#define SIT9531X_VARIANT_ID_95317 0x17
+#define SIT9531X_VARIANT_ID_95316 0x31
+
+#endif /* _SIT9531X_REGS_H */
--
2.43.0
This email message and any attachments are for the sole use of the intended recipient and contain proprietary and/or confidential information that may be privileged or otherwise protected from disclosure. Any unauthorized review, use, disclosure, copying or distribution of the information included in this message and any attachment is prohibited and may be unlawful. If you are not an intended recipient, please contact the sender by reply email and permanently delete and destroy the original message and any attachments, and any printouts or copies of the message and attachments to the original message. Thank you.
^ permalink raw reply related [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-07-31 18:10 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-31 18:09 [PATCH v3 net-next 0/3] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
2026-07-31 18:09 ` [PATCH v3 net-next 1/3] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
2026-07-31 18:09 ` [PATCH v3 net-next 2/3] dt-bindings: dpll: add SiTime SiT9531x clock generator Ali Rouhi
2026-07-31 18:09 ` [PATCH v3 net-next 3/3] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox