* [PATCH net-next v11 01/13] dt-bindings: dpll: allow hex unit addresses on output pins
2026-09-30 23:37 [PATCH net-next v11 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
@ 2026-09-30 23:37 ` Ali Rouhi
2026-10-02 8:32 ` Krzysztof Kozlowski
2026-09-30 23:37 ` [PATCH net-next v11 03/13] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
` (11 subsequent siblings)
12 siblings, 1 reply; 26+ messages in thread
From: Ali Rouhi @ 2026-09-30 23:37 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi, devicetree,
netdev, linux-kernel
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
The output-pins node matches its children against "^pin@[0-9]+$", while
input-pins takes "^pin@[0-9a-f]+$". A unit address is the reg value in
hex, so a device with more than ten outputs cannot describe the
eleventh: dtc wants pin@a for reg = <10>, and the output pattern refuses
it.
Use the input pattern for the outputs as well.
Fixes: 0afcee10dda1 ("dt-bindings: dpll: Add DPLL device and pin")
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
Documentation/devicetree/bindings/dpll/dpll-device.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Documentation/devicetree/bindings/dpll/dpll-device.yaml b/Documentation/devicetree/bindings/dpll/dpll-device.yaml
index fb8d7a9a3693..4b5184500b9f 100644
--- a/Documentation/devicetree/bindings/dpll/dpll-device.yaml
+++ b/Documentation/devicetree/bindings/dpll/dpll-device.yaml
@@ -65,7 +65,7 @@ properties:
const: 0
patternProperties:
- "^pin@[0-9]+$":
+ "^pin@[0-9a-f]+$":
$ref: /schemas/dpll/dpll-pin.yaml
unevaluatedProperties: false
--
2.43.0
^ permalink raw reply [flat|nested] 26+ messages in thread* Re: [PATCH net-next v11 01/13] dt-bindings: dpll: allow hex unit addresses on output pins
2026-09-30 23:37 ` [PATCH net-next v11 01/13] dt-bindings: dpll: allow hex unit addresses on output pins Ali Rouhi
@ 2026-10-02 8:32 ` Krzysztof Kozlowski
0 siblings, 0 replies; 26+ messages in thread
From: Krzysztof Kozlowski @ 2026-10-02 8:32 UTC (permalink / raw)
To: Ali Rouhi
Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba,
pabeni, robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
devicetree, netdev, linux-kernel
On Wed, Sep 30, 2026 at 11:37:16PM +0000, Ali Rouhi wrote:
> From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
>
> The output-pins node matches its children against "^pin@[0-9]+$", while
> input-pins takes "^pin@[0-9a-f]+$". A unit address is the reg value in
> hex, so a device with more than ten outputs cannot describe the
> eleventh: dtc wants pin@a for reg = <10>, and the output pattern refuses
> it.
>
> Use the input pattern for the outputs as well.
>
> Fixes: 0afcee10dda1 ("dt-bindings: dpll: Add DPLL device and pin")
> Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
> Assisted-by: Claude:claude-4-opus [chat]
: LLM
(see current docs)
With this fixed:
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>
Best regards,
Krzysztof
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH net-next v11 03/13] dt-bindings: dpll: add SiTime SiT95316 clock generator
2026-09-30 23:37 [PATCH net-next v11 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
2026-09-30 23:37 ` [PATCH net-next v11 01/13] dt-bindings: dpll: allow hex unit addresses on output pins Ali Rouhi
@ 2026-09-30 23:37 ` Ali Rouhi
2026-10-05 1:09 ` netdev-bot+sashiko
2026-09-30 23:37 ` [PATCH net-next v11 02/13] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
` (10 subsequent siblings)
12 siblings, 1 reply; 26+ messages in thread
From: Ali Rouhi @ 2026-09-30 23:37 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi, devicetree,
netdev, linux-kernel
Add a binding for the SiTime SiT9531x family of clock generators: an
I2C-controlled device with four independent PLLs, up to eight input
clocks and up to twelve outputs, described as a DPLL provider.
The XO is given either as a clock or, for firmware that describes the
oscillator as a property rather than as a clock provider, such as ACPI,
as clock-frequency.
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
.../bindings/dpll/sitime,sit95316.yaml | 175 ++++++++++++++++++
MAINTAINERS | 6 +
2 files changed, 181 insertions(+)
create mode 100644 Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
diff --git a/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml b/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
new file mode 100644
index 000000000000..997b410e8f52
--- /dev/null
+++ b/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
@@ -0,0 +1,175 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/dpll/sitime,sit95316.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: SiTime SiT95316/SiT95317 DPLL Clock Generator
+
+maintainers:
+ - Ali Rouhi <arouhi@sitime.com>
+
+description: |
+ SiTime SiT95316 and SiT95317 are I2C-controlled programmable clock
+ generators with integrated DPLL for synchronization applications. Both
+ variants contain four PLLs with automatic/manual reference selection,
+ DCO frequency adjustment, and phase offset measurement via an on-chip
+ TDC (Time-to-Digital Converter).
+
+ Both parts have 4 differential input pairs whose lanes can also be
+ driven independently as single-ended references, so 8 inputs are
+ individually selectable. SiT95317 drives 8 outputs, SiT95316
+ drives 12.
+
+ Pin numbering. An input pin's reg is its lane: 2k is INkP and 2k + 1
+ is INkN, 0 to 7. A pair the loaded configuration runs differential is
+ one input, described at 2k. An output pin's reg is its physical output
+ OUTn, 0 to 11; the SiT95317 bonds out OUT0, OUT3, OUT4, OUT5, OUT7, OUT8,
+ OUT9 and OUT11.
+
+properties:
+ compatible:
+ enum:
+ - sitime,sit95316
+ - sitime,sit95317
+
+ reg:
+ maxItems: 1
+
+ clocks:
+ maxItems: 1
+
+ clock-names:
+ items:
+ - const: xtal
+
+ clock-frequency:
+ description:
+ XO rate in Hz feeding XIN/XO_CLK, for firmware that describes the
+ oscillator as a property rather than as a clock provider, such as
+ ACPI. Use either this or "clocks", not both.
+
+ 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.
+
+ dpll-types:
+ maxItems: 4
+
+ input-pins:
+ type: object
+ patternProperties:
+ "^pin@[0-9a-f]+$":
+ type: object
+ properties:
+ reg:
+ maximum: 7
+
+required:
+ - compatible
+ - reg
+
+dependencies:
+ clocks: [ clock-names ]
+
+oneOf:
+ - required:
+ - clocks
+ - required:
+ - clock-frequency
+
+allOf:
+ - $ref: /schemas/dpll/dpll-device.yaml#
+ - if:
+ properties:
+ compatible:
+ const: sitime,sit95317
+ then:
+ properties:
+ output-pins:
+ patternProperties:
+ "^pin@[0-9a-f]+$":
+ properties:
+ reg:
+ enum: [0, 3, 4, 5, 7, 8, 9, 11]
+ else:
+ properties:
+ output-pins:
+ patternProperties:
+ "^pin@[0-9a-f]+$":
+ properties:
+ reg:
+ maximum: 11
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ dpll@68 {
+ compatible = "sitime,sit95316";
+ reg = <0x68>;
+ clocks = <&xo2>;
+ clock-names = "xtal";
+ reset-gpios = <&gpio 78 GPIO_ACTIVE_LOW>;
+ interrupts = <12 IRQ_TYPE_LEVEL_LOW>;
+ dpll-types = "eec", "eec", "eec", "eec";
+
+ input-pins {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ /* IN0P and IN0N: pair 0 runs single-ended */
+ 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";
+ supported-frequencies-hz = /bits/ 64 <156250000>;
+ };
+
+ pin@1 {
+ reg = <1>;
+ label = "clkout1";
+ supported-frequencies-hz = /bits/ 64 <25000000>;
+ };
+
+ pin@a {
+ reg = <10>;
+ label = "clkout10";
+ supported-frequencies-hz = /bits/ 64 <10000000>;
+ };
+ };
+ };
+ };
+...
diff --git a/MAINTAINERS b/MAINTAINERS
index cae5962045dd..b4a901e65bfd 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25151,6 +25151,12 @@ S: Maintained
W: http://www.winischhofer.at/linuxsisusbvga.shtml
F: drivers/usb/misc/sisusbvga/
+SITIME SIT9531X DPLL DRIVER
+M: Ali Rouhi <arouhi@sitime.com>
+L: netdev@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
+
SL28 CPLD MFD DRIVER
M: Michael Walle <mwalle@kernel.org>
S: Maintained
--
2.43.0
^ permalink raw reply [flat|nested] 26+ messages in thread* Re: [PATCH net-next v11 03/13] dt-bindings: dpll: add SiTime SiT95316 clock generator
2026-09-30 23:37 ` [PATCH net-next v11 03/13] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
@ 2026-10-05 1:09 ` netdev-bot+sashiko
0 siblings, 0 replies; 26+ messages in thread
From: netdev-bot+sashiko @ 2026-10-05 1:09 UTC (permalink / raw)
To: arouhi
Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba,
pabeni, robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
devicetree, netdev, linux-kernel
Thank you for your contribution! Sashiko AI review found more severe
issue(s) in other patches of this series. This mail is only an FYI, in case
you want to address the nit picks below when you respin.
Low priority / nit picks for your consideration:
- [Low] Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
defines no power-supply properties (for example a core VDD supply and…
--
dt-bindings: dpll: add SiTime SiT95316 clock generator
This adds a devicetree binding for the SiTime SiT95316/SiT95317 clock
generators. They are I2C-controlled DPLL providers with four PLLs, up to
eight inputs and up to twelve outputs. The XO is described either as a
clock or as a clock-frequency property.
> diff --git a/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml b/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
> new file mode 100644
> index 0000000000000..997b410e8f526
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
[ ... ]
> +properties:
> + compatible:
> + enum:
> + - sitime,sit95316
> + - sitime,sit95317
> +
> + reg:
> + maxItems: 1
> +
> + clocks:
> + maxItems: 1
> +
> + clock-names:
> + items:
> + - const: xtal
> +
> + clock-frequency:
> + description:
> + XO rate in Hz feeding XIN/XO_CLK, for firmware that describes the
> + oscillator as a property rather than as a clock provider, such as
> + ACPI. Use either this or "clocks", not both.
> +
> + 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.
> +
> + dpll-types:
> + maxItems: 4
> +
> + input-pins:
[Severity: Low]
This isn't a bug, but should the binding describe the chip's power
supplies here? For example a core VDD supply and the per-bank VDDO
output supplies.
The schema ends with:
unevaluatedProperties: false
With that, a board that adds a *-supply property to the dpll node fails
dt_binding_check. A board that powers the part from switchable regulators
then has no valid way to describe those rails. Without them, a driver
cannot make sure the rails are on before it releases RESETB and starts
I2C access during probe.
Documentation/devicetree/bindings/writing-bindings.rst says:
DO attempt to make bindings complete even if a driver doesn't
support some features.
The supply names were not checked against the SiTime datasheet. Any
supply properties added should use the chip's actual VDD/VDDO pin
names.
The existing microchip,zl30731.yaml DPLL binding also leaves out
supplies, so there is precedent for this in the subsystem. Optional
supply properties could also be added later without breaking existing
DTs.
[ ... ]
> +unevaluatedProperties: false
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260930233714.87679-1-arouhi%40sitime.com
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH net-next v11 02/13] dt-bindings: vendor-prefixes: add SiTime Corporation
2026-09-30 23:37 [PATCH net-next v11 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
2026-09-30 23:37 ` [PATCH net-next v11 01/13] dt-bindings: dpll: allow hex unit addresses on output pins Ali Rouhi
2026-09-30 23:37 ` [PATCH net-next v11 03/13] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
@ 2026-09-30 23:37 ` Ali Rouhi
2026-09-30 23:37 ` [PATCH net-next v11 04/13] dpll: add basic SiTime SiT9531x support Ali Rouhi
` (9 subsequent siblings)
12 siblings, 0 replies; 26+ messages in thread
From: Ali Rouhi @ 2026-09-30 23:37 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi, devicetree,
netdev, linux-kernel
Add vendor prefix for SiTime Corporation, manufacturer of
programmable clock generators and MEMS oscillators.
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
Acked-by: Conor Dooley <conor.dooley@microchip.com>
---
Documentation/devicetree/bindings/vendor-prefixes.yaml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml
index ba2002969373..65da27d6e84d 100644
--- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
+++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
@@ -1563,6 +1563,8 @@ patternProperties:
description: SiRF Technology, Inc.
"^sis,.*":
description: Silicon Integrated Systems Corp.
+ "^sitime,.*":
+ description: SiTime Corporation
"^sitronix,.*":
description: Sitronix Technology Corporation
"^skov,.*":
--
2.43.0
^ permalink raw reply [flat|nested] 26+ messages in thread* [PATCH net-next v11 04/13] dpll: add basic SiTime SiT9531x support
2026-09-30 23:37 [PATCH net-next v11 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (2 preceding siblings ...)
2026-09-30 23:37 ` [PATCH net-next v11 02/13] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
@ 2026-09-30 23:37 ` Ali Rouhi
2026-10-05 1:09 ` netdev-bot+sashiko
2026-09-30 23:37 ` [PATCH net-next v11 05/13] dpll: sit9531x: read DPLL types and pin properties from system firmware Ali Rouhi
` (8 subsequent siblings)
12 siblings, 1 reply; 26+ messages in thread
From: Ali Rouhi @ 2026-09-30 23:37 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi, devicetree,
netdev, linux-kernel
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
The SiT9531x is an I2C clock generator with four independent PLLs, up to
eight input clocks and up to twelve outputs. Later patches register the
PLLs with the DPLL subsystem and expose the clocks as pins; this one adds
only what is needed to reach the device.
The register space is paged: 32 pages of 256 registers, selected by
writing the page number to offset 0xFF, which is present in every page.
A regmap range configuration describes that window, so the rest of the
driver addresses a register as a page and an offset and never touches the
selector itself. Pages come in pairs for the PLLs (0x0A/0x1A for PLLA,
and so on).
Probe reads the rate of the crystal feeding XIN, since every frequency
the driver later computes derives from it; takes the optional reset line
and leaves it deasserted, because the device configuration comes from
efuse or from a blob applied before probe and a reset would discard it;
then identifies the variant from the single byte at page 0 offset 0x02
and refuses to bind on anything unknown.
Suggested-by: Ivan Vecera <ivecera@redhat.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 | 1 +
drivers/dpll/Kconfig | 2 +
drivers/dpll/Makefile | 1 +
drivers/dpll/sit9531x/Kconfig | 13 ++
drivers/dpll/sit9531x/Makefile | 4 +
drivers/dpll/sit9531x/core.c | 320 +++++++++++++++++++++++++++++++++
drivers/dpll/sit9531x/core.h | 91 ++++++++++
drivers/dpll/sit9531x/regs.h | 44 +++++
8 files changed, 476 insertions(+)
create mode 100644 drivers/dpll/sit9531x/Kconfig
create mode 100644 drivers/dpll/sit9531x/Makefile
create mode 100644 drivers/dpll/sit9531x/core.c
create mode 100644 drivers/dpll/sit9531x/core.h
create mode 100644 drivers/dpll/sit9531x/regs.h
diff --git a/MAINTAINERS b/MAINTAINERS
index b4a901e65bfd..b500dbb67c4e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25156,6 +25156,7 @@ M: Ali Rouhi <arouhi@sitime.com>
L: netdev@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
+F: drivers/dpll/sit9531x/
SL28 CPLD MFD DRIVER
M: Michael Walle <mwalle@kernel.org>
diff --git a/drivers/dpll/Kconfig b/drivers/dpll/Kconfig
index be98969f040a..f8f7ca121b93 100644
--- a/drivers/dpll/Kconfig
+++ b/drivers/dpll/Kconfig
@@ -23,6 +23,8 @@ config DPLL_REFCNT_TRACKER
If unsure, say N.
+source "drivers/dpll/sit9531x/Kconfig"
+
source "drivers/dpll/zl3073x/Kconfig"
endmenu
diff --git a/drivers/dpll/Makefile b/drivers/dpll/Makefile
index 9e7a3a3e592e..4adc50d748d4 100644
--- a/drivers/dpll/Makefile
+++ b/drivers/dpll/Makefile
@@ -8,4 +8,5 @@ dpll-y += dpll_core.o
dpll-y += dpll_netlink.o
dpll-y += dpll_nl.o
+obj-$(CONFIG_SIT9531X_DPLL) += sit9531x/
obj-$(CONFIG_ZL3073X) += zl3073x/
diff --git a/drivers/dpll/sit9531x/Kconfig b/drivers/dpll/sit9531x/Kconfig
new file mode 100644
index 000000000000..ac5b7f31a034
--- /dev/null
+++ b/drivers/dpll/sit9531x/Kconfig
@@ -0,0 +1,13 @@
+# 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).
+
+ To compile this driver as a module, choose M here: the
+ module will be called sit9531x.
diff --git a/drivers/dpll/sit9531x/Makefile b/drivers/dpll/sit9531x/Makefile
new file mode 100644
index 000000000000..a221fe55386a
--- /dev/null
+++ b/drivers/dpll/sit9531x/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+obj-$(CONFIG_SIT9531X_DPLL) += sit9531x.o
+sit9531x-y := core.o
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
new file mode 100644
index 000000000000..c7c14442df92
--- /dev/null
+++ b/drivers/dpll/sit9531x/core.c
@@ -0,0 +1,320 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * SiTime SiT9531x DPLL core driver
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * I2C probe, paged regmap configuration and register access helpers.
+ */
+
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/device.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+
+#include "core.h"
+#include "regs.h"
+
+#define SIT9531X_CHIP(_id, _nin, _nout, _name, _map) \
+ { .id = (_id), .num_inputs = (_nin), .num_outputs = (_nout), \
+ .name = (_name), .clkout_map = (_map) }
+
+/* Per-variant output index -> physical slot mapping */
+static const u8 clkout_map_95317[] = {0, 3, 4, 5, 7, 8, 9, 11};
+static const u8 clkout_map_95316[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
+
+static const struct sit9531x_chip_info sit9531x_chip_ids[] = {
+ SIT9531X_CHIP(SIT9531X_VARIANT_ID_95317, 8, 8, "SiT95317",
+ clkout_map_95317),
+ SIT9531X_CHIP(SIT9531X_VARIANT_ID_95316, 8, 12, "SiT95316",
+ clkout_map_95316),
+};
+
+#define SIT9531X_RANGE_OFFSET SIT9531X_PAGE_SIZE
+
+/*
+ * Everything the device holds can change without the driver writing it,
+ * so nothing here is cacheable except the page selector. Caching that
+ * one spares a read of it before every access: the range code selects
+ * the page through a read-modify-write, and with no cache that read goes
+ * to the bus each time.
+ *
+ * The cached value is only as good as the last transfer, though. A
+ * transfer that fails can leave the part on another page than the cache
+ * records, so every accessor drops the entry on an error and the next
+ * access reads the selector again.
+ */
+static bool sit9531x_volatile_reg(struct device *dev __maybe_unused,
+ unsigned int reg)
+{
+ return reg != SIT9531X_PAGE_SEL;
+}
+
+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,
+ .volatile_reg = sit9531x_volatile_reg,
+ .cache_type = REGCACHE_MAPLE,
+};
+
+static void sit9531x_page_cache_drop(struct sit9531x_dev *sitdev)
+{
+ regcache_drop_region(sitdev->regmap, SIT9531X_PAGE_SEL,
+ SIT9531X_PAGE_SEL);
+}
+
+/*
+ * sit9531x_read_u8 - read an 8-bit register
+ * @reg: register in SIT9531X_REG(page, offset) form
+ * @val: output value
+ */
+int sit9531x_read_u8(struct sit9531x_dev *sitdev, unsigned int reg,
+ u8 *val)
+{
+ unsigned int vreg, tmp;
+ int rc;
+
+ vreg = (SIT9531X_REG_PAGE(reg) * SIT9531X_PAGE_SIZE) +
+ SIT9531X_REG_OFFSET(reg) + SIT9531X_RANGE_OFFSET;
+
+ rc = regmap_read(sitdev->regmap, vreg, &tmp);
+ if (rc) {
+ sit9531x_page_cache_drop(sitdev);
+ dev_err(sitdev->dev,
+ "Failed to read page 0x%02x reg 0x%02x: %d\n",
+ SIT9531X_REG_PAGE(reg), SIT9531X_REG_OFFSET(reg), rc);
+ } else {
+ *val = (u8)tmp;
+ }
+
+ return rc;
+}
+
+/*
+ * sit9531x_write_u8 - write an 8-bit register
+ * @reg: register in SIT9531X_REG(page, offset) form
+ * @val: value to write
+ */
+int sit9531x_write_u8(struct sit9531x_dev *sitdev, unsigned int reg,
+ u8 val)
+{
+ unsigned int vreg;
+ int rc;
+
+ vreg = (SIT9531X_REG_PAGE(reg) * SIT9531X_PAGE_SIZE) +
+ SIT9531X_REG_OFFSET(reg) + SIT9531X_RANGE_OFFSET;
+
+ rc = regmap_write(sitdev->regmap, vreg, val);
+ if (rc) {
+ sit9531x_page_cache_drop(sitdev);
+ dev_err(sitdev->dev,
+ "Failed to write page 0x%02x reg 0x%02x: %d\n",
+ SIT9531X_REG_PAGE(reg), SIT9531X_REG_OFFSET(reg), rc);
+ }
+
+ return rc;
+}
+
+/*
+ * sit9531x_read_pll_u8 - read a register on a PLL page
+ * @val: output value
+ */
+int sit9531x_read_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 offset, u8 *val)
+{
+ u8 page = sit9531x_pll_page(pll_idx);
+
+ return sit9531x_read_u8(sitdev, SIT9531X_REG(page, offset), val);
+}
+
+/*
+ * sit9531x_write_pll_u8 - write a register on a PLL page
+ * @val: value to write
+ */
+int sit9531x_write_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 offset, u8 val)
+{
+ u8 page = sit9531x_pll_page(pll_idx);
+
+ return sit9531x_write_u8(sitdev, SIT9531X_REG(page, offset), val);
+}
+
+/*
+ * sit9531x_update_pll_u8 - read-modify-write a register on a PLL page
+ * @mask: bits to modify
+ * @val: new value for masked bits
+ */
+int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 offset, u8 mask, u8 val)
+{
+ u8 page = sit9531x_pll_page(pll_idx);
+ unsigned int vreg;
+ int rc;
+
+ vreg = (page * SIT9531X_PAGE_SIZE) + offset + SIT9531X_RANGE_OFFSET;
+
+ /*
+ * A read-modify-write is a read and a write, and either can leave
+ * the cached page selector wrong, so fail the way the single
+ * accessors do.
+ */
+ rc = regmap_update_bits(sitdev->regmap, vreg, mask, val);
+ if (rc) {
+ sit9531x_page_cache_drop(sitdev);
+ dev_err(sitdev->dev,
+ "Failed to update page 0x%02x reg 0x%02x: %d\n",
+ page, offset, rc);
+ }
+
+ return rc;
+}
+
+static int sit9531x_read_variant_id(struct sit9531x_dev *sitdev, u8 *id)
+{
+ return sit9531x_read_u8(sitdev, SIT9531X_REG_VARIANT_ID, id);
+}
+
+static const struct sit9531x_chip_info *sit9531x_match_variant(u8 id)
+{
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(sit9531x_chip_ids); i++) {
+ if (sit9531x_chip_ids[i].id == id)
+ return &sit9531x_chip_ids[i];
+ }
+
+ return NULL;
+}
+
+int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
+{
+ struct clk *xtal_clk;
+ u8 variant_id;
+ int rc;
+
+ /*
+ * Fvco = Fref * (DIVN + frac/2^32) with Fref derived from the XO
+ * feeding XIN/XO_CLK, so the rate is needed before anything can be
+ * computed from a divider. The rate normally comes from a "clocks"
+ * phandle (clock-names = "xtal"). On platforms where the firmware
+ * does not expose the XO through the clock framework, fall back to
+ * a "clock-frequency" device property.
+ */
+ xtal_clk = devm_clk_get_optional_enabled(sitdev->dev, "xtal");
+ if (IS_ERR(xtal_clk))
+ return dev_err_probe(sitdev->dev, PTR_ERR(xtal_clk),
+ "Failed to get xtal clock\n");
+ sitdev->xtal_freq = clk_get_rate(xtal_clk);
+ if (!sitdev->xtal_freq) {
+ u32 freq;
+
+ if (!device_property_read_u32(sitdev->dev, "clock-frequency",
+ &freq))
+ sitdev->xtal_freq = freq;
+ }
+ if (!sitdev->xtal_freq)
+ return dev_err_probe(sitdev->dev, -EINVAL,
+ "no xtal rate: provide clocks=<&xo> + clock-names=\"xtal\", or a clock-frequency property\n");
+
+ /*
+ * Held deasserted, never pulsed: the chip configuration comes from
+ * efuse or an NVM blob applied before probe, and a reset would
+ * discard it. Must precede the first I2C access, as a board that
+ * powers up asserted keeps the chip unreachable until released.
+ */
+ sitdev->reset_gpio = devm_gpiod_get_optional(sitdev->dev, "reset",
+ GPIOD_OUT_LOW);
+ if (IS_ERR(sitdev->reset_gpio))
+ return dev_err_probe(sitdev->dev, PTR_ERR(sitdev->reset_gpio),
+ "Failed to request reset gpio\n");
+ if (sitdev->reset_gpio)
+ fsleep(10000); /* internal boot after release */
+
+ rc = sit9531x_read_variant_id(sitdev, &variant_id);
+ if (rc)
+ return rc;
+
+ sitdev->info = sit9531x_match_variant(variant_id);
+ if (!sitdev->info)
+ return dev_err_probe(sitdev->dev, -ENODEV,
+ "Unknown variant ID: 0x%02x\n",
+ variant_id);
+
+ rc = devm_mutex_init(sitdev->dev, &sitdev->multiop_lock);
+ if (rc)
+ return dev_err_probe(sitdev->dev, rc,
+ "Failed to initialize mutex\n");
+
+ dev_info(sitdev->dev, "%s detected, %u inputs, %u outputs\n",
+ sitdev->info->name, sitdev->info->num_inputs,
+ sitdev->info->num_outputs);
+
+ return 0;
+}
+
+static int sit9531x_i2c_probe(struct i2c_client *client)
+{
+ struct sit9531x_dev *sitdev;
+ struct regmap *regmap;
+
+ regmap = devm_regmap_init_i2c(client, &sit9531x_regmap_config);
+ if (IS_ERR(regmap))
+ return dev_err_probe(&client->dev, PTR_ERR(regmap),
+ "Failed to initialize regmap\n");
+
+ sitdev = devm_kzalloc(&client->dev, sizeof(*sitdev), GFP_KERNEL);
+ if (!sitdev)
+ return -ENOMEM;
+
+ sitdev->dev = &client->dev;
+ sitdev->client = client;
+ sitdev->regmap = regmap;
+ i2c_set_clientdata(client, sitdev);
+
+ return sit9531x_dev_probe(sitdev);
+}
+
+static const struct of_device_id sit9531x_of_match[] = {
+ { .compatible = "sitime,sit95316" },
+ { .compatible = "sitime,sit95317" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, sit9531x_of_match);
+
+static struct i2c_driver sit9531x_i2c_driver = {
+ .driver = {
+ .name = "sit9531x",
+ .of_match_table = sit9531x_of_match,
+ },
+ .probe = sit9531x_i2c_probe,
+};
+module_i2c_driver(sit9531x_i2c_driver);
+
+MODULE_AUTHOR("Ali Rouhi <arouhi@sitime.com>");
+MODULE_AUTHOR("Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>");
+MODULE_DESCRIPTION("SiTime SiT9531x DPLL subsystem driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
new file mode 100644
index 000000000000..76a2632f0ce4
--- /dev/null
+++ b/drivers/dpll/sit9531x/core.h
@@ -0,0 +1,91 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * SiTime SiT9531x DPLL core driver
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * Device structure, register access helpers, and core function
+ * declarations.
+ */
+
+#ifndef _SIT9531X_CORE_H
+#define _SIT9531X_CORE_H
+
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/mutex.h>
+#include <linux/regmap.h>
+#include <linux/types.h>
+
+#include "regs.h"
+
+#define SIT9531X_NUM_PLLS 4
+#define SIT9531X_MAX_INPUTS 8
+#define SIT9531X_MAX_OUTPUTS 12
+
+/*
+ * struct sit9531x_chip_info - chip variant identification
+ * @id: variant ID byte read from register
+ * @num_inputs: number of input clock pins
+ * @num_outputs: number of output clock pins
+ * @name: human-readable variant name
+ * @clkout_map: per-output slot mapping (output index -> physical slot)
+ */
+struct sit9531x_chip_info {
+ u8 id;
+ u8 num_inputs;
+ u8 num_outputs;
+ const char *name;
+ const u8 *clkout_map;
+};
+
+/*
+ * struct sit9531x_dev - SiT9531x device instance
+ * @dev: parent device
+ * @client: I2C client
+ * @regmap: paged register map
+ * @info: detected chip variant info
+ * @multiop_lock: serializes multi-register sequences
+ * @xtal_freq: crystal oscillator frequency in Hz
+ * @reset_gpio: optional reset line (DT "reset-gpios"), NULL if absent
+ */
+struct sit9531x_dev {
+ struct device *dev;
+ struct i2c_client *client;
+ struct regmap *regmap;
+ const struct sit9531x_chip_info *info;
+ /* Serializes multi-step register sequences */
+ struct mutex multiop_lock;
+
+ u32 xtal_freq;
+
+ struct gpio_desc *reset_gpio;
+};
+
+/*
+ * sit9531x_pll_page - get register page for PLL index
+ * @pll_idx: PLL index (0 = PLLA, 3 = PLLD)
+ */
+static inline u8 sit9531x_pll_page(u8 pll_idx)
+{
+ return SIT9531X_PAGE_PLLA + pll_idx;
+}
+
+extern const struct regmap_config sit9531x_regmap_config;
+
+/* ---- Core lifecycle ---- */
+int sit9531x_dev_probe(struct sit9531x_dev *sitdev);
+
+/* ---- Register access ---- */
+int sit9531x_read_u8(struct sit9531x_dev *sitdev, unsigned int reg, u8 *val);
+int sit9531x_write_u8(struct sit9531x_dev *sitdev, unsigned int reg, u8 val);
+int sit9531x_read_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx, u8 offset,
+ u8 *val);
+int sit9531x_write_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx, u8 offset,
+ u8 val);
+int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx, u8 offset,
+ u8 mask, u8 val);
+
+#endif /* _SIT9531X_CORE_H */
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
new file mode 100644
index 000000000000..67077d112653
--- /dev/null
+++ b/drivers/dpll/sit9531x/regs.h
@@ -0,0 +1,44 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * SiTime SiT9531x register definitions
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ */
+
+#ifndef _SIT9531X_REGS_H
+#define _SIT9531X_REGS_H
+
+/*
+ * I2C register model:
+ * - Page select register at offset 0xFF, present in every page
+ * - Each page has 256 registers (0x00-0xFF)
+ * - Some pages are paired (e.g. 0x0A/0x1A for PLLA)
+ */
+#define SIT9531X_PAGE_SEL 0xFF
+#define SIT9531X_PAGE_SIZE 0x100
+#define SIT9531X_NUM_PAGES 32
+
+/* Helper macros for page:offset addressing */
+#define SIT9531X_REG(_page, _offset) (((_page) << 8) | (_offset))
+#define SIT9531X_REG_PAGE(_reg) ((_reg) >> 8)
+#define SIT9531X_REG_OFFSET(_reg) ((_reg) & 0xFF)
+
+#define SIT9531X_PAGE_OUTSYS0 0x03
+#define SIT9531X_PAGE_OUTSYS1 0x04
+#define SIT9531X_PAGE_PLLA 0x0A
+#define SIT9531X_PAGE_PLLA_EXT 0x1A
+
+/*
+ * VARIANT_ID is a single byte at page 0 reg 0x02 (95317 = 0x17, 95316 = 0x31).
+ * Reg 0x03 carries an unrelated revision byte and must not be combined into
+ * the variant identifier.
+ */
+#define SIT9531X_REG_VARIANT_ID SIT9531X_REG(0x00, 0x02)
+
+/* Variant ID values (page 0 reg 0x02) */
+#define SIT9531X_VARIANT_ID_95317 0x17
+#define SIT9531X_VARIANT_ID_95316 0x31
+
+#endif /* _SIT9531X_REGS_H */
--
2.43.0
^ permalink raw reply [flat|nested] 26+ messages in thread* Re: [PATCH net-next v11 04/13] dpll: add basic SiTime SiT9531x support
2026-09-30 23:37 ` [PATCH net-next v11 04/13] dpll: add basic SiTime SiT9531x support Ali Rouhi
@ 2026-10-05 1:09 ` netdev-bot+sashiko
0 siblings, 0 replies; 26+ messages in thread
From: netdev-bot+sashiko @ 2026-10-05 1:09 UTC (permalink / raw)
To: arouhi
Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba,
pabeni, robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
devicetree, netdev, linux-kernel
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 1 · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] In drivers/dpll/sit9531x/core.c, the page selector (physical
0xFF) is the only cached register: sit9531x_volatile_reg() returns…
--
dpll: add basic SiTime SiT9531x support
This adds the first I2C driver for the SiTime SiT9531x clock generator.
It sets up a paged regmap with the page selector at offset 0xFF, so the
register accessors address a register by page and offset. Probe reads
the crystal rate, takes the optional reset GPIO and identifies the
variant from page 0 offset 0x02.
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> new file mode 100644
> index 0000000000000..c7c14442df922
> --- /dev/null
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -0,0 +1,320 @@
[ ... ]
> +/*
> + * Everything the device holds can change without the driver writing it,
> + * so nothing here is cacheable except the page selector. Caching that
> + * one spares a read of it before every access: the range code selects
> + * the page through a read-modify-write, and with no cache that read goes
> + * to the bus each time.
> + *
> + * The cached value is only as good as the last transfer, though. A
> + * transfer that fails can leave the part on another page than the cache
> + * records, so every accessor drops the entry on an error and the next
> + * access reads the selector again.
> + */
> +static bool sit9531x_volatile_reg(struct device *dev __maybe_unused,
> + unsigned int reg)
> +{
> + return reg != SIT9531X_PAGE_SEL;
> +}
[ ... ]
> +static void sit9531x_page_cache_drop(struct sit9531x_dev *sitdev)
> +{
> + regcache_drop_region(sitdev->regmap, SIT9531X_PAGE_SEL,
> + SIT9531X_PAGE_SEL);
> +}
[ ... ]
> + rc = regmap_read(sitdev->regmap, vreg, &tmp);
> + if (rc) {
> + sit9531x_page_cache_drop(sitdev);
> + dev_err(sitdev->dev,
> + "Failed to read page 0x%02x reg 0x%02x: %d\n",
> + SIT9531X_REG_PAGE(reg), SIT9531X_REG_OFFSET(reg), rc);
[Severity: Medium]
Does dropping the cache entry on error keep the cached page selector in
sync with the chip? The comment above sit9531x_volatile_reg() says it
does, but some paths seem to be missed.
_regmap_select_page() sets the selector with
_regmap_update_bits(..., force=false). When the cached value already
matches the target page, no selector write reaches the bus. With a stale
cache entry, the access goes to whatever page the chip is really on, and
the I2C transfer still succeeds.
The first case is an adapter that only supports SMBus byte data. There,
regmap_get_i2c_bus() picks regmap_smbus_byte, so map->reg_write is
_regmap_bus_reg_write and defer_caching is false. _regmap_write() then
updates the cache before the transfer:
drivers/base/regmap/regmap.c:_regmap_write() {
...
if (!map->cache_bypass && !map->defer_caching) {
ret = regcache_write(map, reg, val);
...
ret = map->reg_write(context, reg, val);
...
}
_regmap_bus_reg_write() does not invalidate the entry when the transfer
fails. A failed selector write therefore leaves the cache at page N while
the chip stays on page P.
sit9531x_page_cache_drop() only runs after regmap_read(), regmap_write()
or regmap_update_bits() has returned and released map->lock. Other
regmap users, such as the regmap debugfs registers file, can see the
stale entry in that window.
If the failed selector write happens inside a debugfs read, nothing
drops the entry at all. Can the next sit9531x_write_u8() to page N then
land on page P without any error?
Adapters with full I2C support use the raw path. They look safe, because
_regmap_raw_write_impl() drops the cache under the lock when a write
fails. Later patches in the series also put the driver's own runtime
accessors under multiop_lock. So only regmap users outside the driver,
such as debugfs, can reach that window.
The second case is a selector change with no I2C error at all. Examples
are RESETB driven by board logic or a BMC, a brown-out, or an NVM reload.
The chip comes back on page 0 while the cache still holds the old page.
A later patch in the series drops the entry on resume. Nothing covers a
reset the driver cannot see, though. After one, could writes meant for
the PLL pages 0x0A-0x0D go to page 0 instead?
Would it be simpler to make SIT9531X_PAGE_SEL volatile as well?
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260930233714.87679-1-arouhi%40sitime.com
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH net-next v11 05/13] dpll: sit9531x: read DPLL types and pin properties from system firmware
2026-09-30 23:37 [PATCH net-next v11 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (3 preceding siblings ...)
2026-09-30 23:37 ` [PATCH net-next v11 04/13] dpll: add basic SiTime SiT9531x support Ali Rouhi
@ 2026-09-30 23:37 ` Ali Rouhi
2026-10-05 1:09 ` netdev-bot+sashiko
2026-09-30 23:37 ` [PATCH net-next v11 06/13] dpll: sit9531x: register DPLL devices and pins Ali Rouhi
` (7 subsequent siblings)
12 siblings, 1 reply; 26+ messages in thread
From: Ali Rouhi @ 2026-09-30 23:37 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi, devicetree,
netdev, linux-kernel
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
The DPLL core wants a type per device and a property set per pin: a
label, the direction, the capabilities, and the frequencies the pin
supports. None of that can be read from the chip -- which frequencies a
board actually presents on a given input is a board fact -- so they come
from the firmware node, with defaults for a node that does not describe
them.
Input pins are interleaved P and N lanes of four differential pairs, so a
logical index maps to a pair and a lane, and a pair configured
single-ended presents two independent inputs where a differential one
presents one. The labels follow from that, and the two extra input
positions -- the crystal and the inter-PLL sync net -- are named
separately.
One of the advertised properties is worth naming. An output whose node
lists no supported frequencies is advertised as a continuous range rather
than nothing at all, since those pins do accept a frequency set; a
request inside the range that the integer output divider cannot produce
is refused when it is made.
Kept in its own file, and introduced before anything is registered, so
the registration code that follows has nothing to say about firmware.
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
drivers/dpll/sit9531x/Makefile | 2 +-
drivers/dpll/sit9531x/core.h | 68 ++++++
drivers/dpll/sit9531x/prop.c | 427 +++++++++++++++++++++++++++++++++
drivers/dpll/sit9531x/prop.h | 37 +++
4 files changed, 533 insertions(+), 1 deletion(-)
create mode 100644 drivers/dpll/sit9531x/prop.c
create mode 100644 drivers/dpll/sit9531x/prop.h
diff --git a/drivers/dpll/sit9531x/Makefile b/drivers/dpll/sit9531x/Makefile
index a221fe55386a..819af61123f5 100644
--- a/drivers/dpll/sit9531x/Makefile
+++ b/drivers/dpll/sit9531x/Makefile
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_SIT9531X_DPLL) += sit9531x.o
-sit9531x-y := core.o
+sit9531x-y := core.o prop.o
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 76a2632f0ce4..4b4f72c8622b 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -23,7 +23,17 @@
#define SIT9531X_NUM_PLLS 4
#define SIT9531X_MAX_INPUTS 8
+#define SIT9531X_NUM_INPUT_PAIRS (SIT9531X_MAX_INPUTS / 2)
#define SIT9531X_MAX_OUTPUTS 12
+/*
+ * INTSYNC (the inter-PLL sync net) is modeled as two pins. The
+ * destination PLL that locks to INTSYNC sees an input pin
+ * (SIT9531X_INTSYNC_PIN_ID, in the input id namespace after the physical
+ * inputs and the xtal); the source PLL that drives INTSYNC sees an output
+ * pin (SIT9531X_INTSYNC_OUT_PIN_ID, appended after the physical outputs).
+ */
+#define SIT9531X_INTSYNC_PIN_ID (SIT9531X_MAX_INPUTS + 1)
+#define SIT9531X_INTSYNC_OUT_PIN_ID SIT9531X_MAX_OUTPUTS
/*
* struct sit9531x_chip_info - chip variant identification
@@ -41,6 +51,35 @@ struct sit9531x_chip_info {
const u8 *clkout_map;
};
+/*
+ * enum sit9531x_signal_mode - input signal electrical mode
+ * @SIT9531X_MODE_SE: single-ended
+ * @SIT9531X_MODE_DE: differential
+ */
+enum sit9531x_signal_mode {
+ SIT9531X_MODE_SE = 0,
+ SIT9531X_MODE_DE,
+};
+
+/*
+ * struct sit9531x_ref - input reference state
+ * @freq: configured frequency in Hz
+ * @sig_mode: signal mode of the pair this lane belongs to
+ * (detected from CLKINx_INPUT_MODE at probe)
+ */
+struct sit9531x_ref {
+ u64 freq;
+ enum sit9531x_signal_mode sig_mode;
+};
+
+/*
+ * struct sit9531x_out - output state
+ * @freq: configured frequency in Hz
+ */
+struct sit9531x_out {
+ u64 freq;
+};
+
/*
* struct sit9531x_dev - SiT9531x device instance
* @dev: parent device
@@ -48,6 +87,8 @@ struct sit9531x_chip_info {
* @regmap: paged register map
* @info: detected chip variant info
* @multiop_lock: serializes multi-register sequences
+ * @ref: array of input reference states
+ * @out: array of output states
* @xtal_freq: crystal oscillator frequency in Hz
* @reset_gpio: optional reset line (DT "reset-gpios"), NULL if absent
*/
@@ -59,11 +100,38 @@ struct sit9531x_dev {
/* Serializes multi-step register sequences */
struct mutex multiop_lock;
+ /* Hardware state */
+ struct sit9531x_ref ref[SIT9531X_MAX_INPUTS + 1]; /* +1 for xtal */
+ struct sit9531x_out out[SIT9531X_MAX_OUTPUTS];
u32 xtal_freq;
struct gpio_desc *reset_gpio;
};
+/*
+ * Logical input pins are interleaved: even index = P lane, odd
+ * index = N lane of pair index/2 (IN0P, IN0N, IN1P, IN1N, ...).
+ * Index SIT9531X_MAX_INPUTS is the XO input.
+ */
+
+/*
+ * sit9531x_input_pair - get input pair number for a logical input index
+ * @index: logical input pin index
+ */
+static inline u8 sit9531x_input_pair(u8 index)
+{
+ return index >> 1;
+}
+
+/*
+ * sit9531x_input_is_n - check if a logical input index is an N lane
+ * @index: logical input pin index
+ */
+static inline bool sit9531x_input_is_n(u8 index)
+{
+ return index & 1;
+}
+
/*
* sit9531x_pll_page - get register page for PLL index
* @pll_idx: PLL index (0 = PLLA, 3 = PLLD)
diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
new file mode 100644
index 000000000000..3635ea497e41
--- /dev/null
+++ b/drivers/dpll/sit9531x/prop.c
@@ -0,0 +1,427 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * SiTime SiT9531x firmware node property parsing
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * Retrieves pin properties from Device Tree firmware nodes (or
+ * applies defaults when no firmware node exists).
+ */
+
+#include <linux/dev_printk.h>
+#include <linux/dpll.h>
+#include <linux/err.h>
+#include <linux/fwnode.h>
+#include <linux/property.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#include "core.h"
+#include "prop.h"
+
+/*
+ * sit9531x_input_pin_label - fill the package label for an input pin
+ *
+ * Split out so input-naming changes stay local to this helper.
+ */
+static void sit9531x_input_pin_label(struct sit9531x_dev *sitdev,
+ struct sit9531x_pin_props *props, u8 id)
+{
+ u8 pair = sit9531x_input_pair(id);
+
+ if (sitdev->ref[id].sig_mode == SIT9531X_MODE_DE)
+ snprintf(props->package_label,
+ sizeof(props->package_label), "IN%u", pair);
+ else
+ snprintf(props->package_label,
+ sizeof(props->package_label), "IN%u%c", pair,
+ sit9531x_input_is_n(id) ? 'N' : 'P');
+}
+
+/*
+ * sit9531x_prop_pin_package_label_set - generate package label
+ * @dir: pin direction
+ * @id: pin index
+ *
+ * Generates a package label string. Output pins are named "OUT0",
+ * "OUT1", ... Input pins are named after the physical pair and lane:
+ * "IN0P", "IN0N", "IN1P", ... for single-ended lanes, or "IN0",
+ * "IN1", ... when the pair is configured differential (the N lane is
+ * not registered in that case).
+ */
+static void
+sit9531x_prop_pin_package_label_set(struct sit9531x_dev *sitdev,
+ struct sit9531x_pin_props *props,
+ enum dpll_pin_direction dir, u8 id)
+{
+ /* The internal INTSYNC pin has a fixed label */
+ if (dir == DPLL_PIN_DIRECTION_INPUT &&
+ id == SIT9531X_INTSYNC_PIN_ID) {
+ strscpy(props->package_label, "INTSYNC",
+ sizeof(props->package_label));
+ props->dpll_props.package_label = props->package_label;
+ return;
+ }
+
+ /* The internal XO reference has a fixed label */
+ if (dir == DPLL_PIN_DIRECTION_INPUT && id == SIT9531X_MAX_INPUTS) {
+ strscpy(props->package_label, "XO",
+ sizeof(props->package_label));
+ props->dpll_props.package_label = props->package_label;
+ return;
+ }
+
+ /* The internal INTSYNC source (output) pin has a fixed label */
+ if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+ id == SIT9531X_INTSYNC_OUT_PIN_ID) {
+ strscpy(props->package_label, "SYNCOUT",
+ sizeof(props->package_label));
+ props->dpll_props.package_label = props->package_label;
+ return;
+ }
+
+ if (dir == DPLL_PIN_DIRECTION_INPUT)
+ sit9531x_input_pin_label(sitdev, props, id);
+ else
+ /*
+ * Name the chip pin, not the driver's index for it. The
+ * two differ on the variant whose outputs are bonded out
+ * from a subset of the twelve slots, and a package label
+ * that named the index would point at a pin that is not
+ * the one being driven.
+ */
+ snprintf(props->package_label, sizeof(props->package_label),
+ "OUT%u", sitdev->info->clkout_map[id]);
+
+ props->dpll_props.package_label = props->package_label;
+}
+
+/*
+ * sit9531x_prop_pin_fwnode_get - find firmware node for a pin
+ * @dir: pin direction
+ * @id: pin index
+ *
+ * Searches for input-pins/output-pins child nodes in DT, looking for a
+ * child whose "reg" property matches the pin. The binding describes reg
+ * as the hardware index, so an output is matched by the chip slot it
+ * drives rather than by the driver's index for it: on the variant where
+ * the two differ, a board describing the pin it wired would otherwise
+ * have its properties applied to a different one.
+ *
+ * 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";
+ if (id < sitdev->info->num_outputs)
+ id = sitdev->info->clkout_map[id];
+ }
+
+ 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;
+ }
+
+ fwnode_handle_put(pins_node);
+
+ props->fwnode = pin_node;
+
+ dev_dbg(sitdev->dev, "Firmware node for %s %sfound\n",
+ props->package_label, pin_node ? "" : "NOT ");
+
+ return pin_node ? 0 : -ENOENT;
+}
+
+/*
+ * sit9531x_pin_props_get - get pin properties for a given pin
+ * @dir: pin direction (INPUT or OUTPUT)
+ * @index: pin index
+ *
+ * Allocates a pin properties structure, generates a package label,
+ * looks up the firmware node if available, and reads optional
+ * properties (label, connection-type, supported-frequencies-hz).
+ *
+ * Call sit9531x_pin_props_put() to free the returned structure.
+ *
+ * Return: pointer to pin properties on success, error pointer on error
+ */
+struct sit9531x_pin_props *
+sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
+ enum dpll_pin_direction dir, u8 index)
+{
+ struct dpll_pin_frequency *ranges;
+ struct sit9531x_pin_props *props;
+ int i, j, num_freqs = 0, rc;
+ u64 *freqs = NULL;
+ const char *type;
+ u64 curr_freq;
+
+ props = kzalloc_obj(*props, GFP_KERNEL);
+ if (!props)
+ return ERR_PTR(-ENOMEM);
+
+ if (dir == DPLL_PIN_DIRECTION_INPUT &&
+ index == SIT9531X_INTSYNC_PIN_ID) {
+ /*
+ * INTSYNC destination pin: a PLL locks to the INTSYNC net as a
+ * reference, so it can be connected.
+ */
+ 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_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_STATE_CAN_CHANGE;
+ curr_freq = sitdev->ref[index].freq;
+ } else {
+ /*
+ * A synthesized clock output is an external connection with
+ * no more specific meaning; a board that knows better says
+ * so through the pin's connection-type property below.
+ */
+ props->dpll_props.type = DPLL_PIN_TYPE_EXT;
+ props->dpll_props.capabilities =
+ DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+ curr_freq = sitdev->out[index].freq;
+ }
+
+ /* Generate package label */
+ sit9531x_prop_pin_package_label_set(sitdev, props, dir, index);
+
+ /*
+ * Both INTSYNC pins are internal to the chip and have no board-level
+ * wiring, so they take no properties from the firmware node.
+ */
+ if (dir == DPLL_PIN_DIRECTION_INPUT &&
+ (index == SIT9531X_INTSYNC_PIN_ID ||
+ index == SIT9531X_MAX_INPUTS))
+ goto skip_fwnode_props;
+ if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+ index == SIT9531X_INTSYNC_OUT_PIN_ID)
+ goto skip_fwnode_props;
+
+ rc = sit9531x_prop_pin_fwnode_get(sitdev, props, dir, index);
+ if (rc)
+ goto skip_fwnode_props;
+
+ /* Look for "label" property -> board label */
+ fwnode_property_read_string(props->fwnode, "label",
+ &props->dpll_props.board_label);
+
+ /* Look for "connection-type" property -> pin type enum */
+ if (!fwnode_property_read_string(props->fwnode, "connection-type",
+ &type)) {
+ if (!strcmp(type, "ext"))
+ props->dpll_props.type = DPLL_PIN_TYPE_EXT;
+ else if (!strcmp(type, "gnss"))
+ props->dpll_props.type = DPLL_PIN_TYPE_GNSS;
+ else if (!strcmp(type, "int"))
+ props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+ else if (!strcmp(type, "synce"))
+ props->dpll_props.type = DPLL_PIN_TYPE_SYNCE_ETH_PORT;
+ else if (!strcmp(type, "mux"))
+ props->dpll_props.type = DPLL_PIN_TYPE_MUX;
+ else
+ dev_warn(sitdev->dev,
+ "Unknown pin type '%s'\n", type);
+ }
+
+ num_freqs = fwnode_property_count_u64(props->fwnode,
+ "supported-frequencies-hz");
+ if (num_freqs <= 0) {
+ num_freqs = 0;
+ goto skip_fwnode_props;
+ }
+
+ freqs = kcalloc(num_freqs, sizeof(*freqs), GFP_KERNEL);
+ if (!freqs) {
+ rc = -ENOMEM;
+ goto err_alloc_freqs;
+ }
+
+ rc = fwnode_property_read_u64_array(props->fwnode,
+ "supported-frequencies-hz",
+ freqs, num_freqs);
+ if (rc) {
+ dev_warn(sitdev->dev,
+ "failed to parse supported-frequencies-hz for %s: %d\n",
+ props->package_label, rc);
+ goto err_alloc_ranges;
+ }
+
+ /*
+ * Seed an input's runtime ref->freq with the first DT-listed
+ * supported frequency: for an input the board lists the rate that is
+ * physically wired to it first. An output's current rate is what its
+ * divider produces, not an entry of the list it may be set to.
+ */
+ if (num_freqs > 0 && dir == DPLL_PIN_DIRECTION_INPUT &&
+ index != SIT9531X_MAX_INPUTS)
+ curr_freq = freqs[0];
+
+skip_fwnode_props:
+ /* Neither INTSYNC pin carries a frequency attribute */
+ if (dir == DPLL_PIN_DIRECTION_INPUT &&
+ index == SIT9531X_INTSYNC_PIN_ID)
+ return props;
+ if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+ index == SIT9531X_INTSYNC_OUT_PIN_ID)
+ return props;
+
+ /*
+ * Advertise only concrete values from firmware plus current runtime
+ * value. For outputs without a firmware list, publish one wide range as
+ * an explicit fallback because those pins do support frequency_set.
+ */
+ ranges = kcalloc(num_freqs + 2, sizeof(*ranges), GFP_KERNEL);
+ if (!ranges) {
+ rc = -ENOMEM;
+ goto err_alloc_ranges;
+ }
+
+ /*
+ * Publish the seeded rate only once the pin is certain to be built.
+ * The allocation above is the last thing that can fail, and a call
+ * that reports failure must not leave the device's cached rate
+ * changed behind it.
+ */
+ if (curr_freq) {
+ if (dir == DPLL_PIN_DIRECTION_INPUT)
+ sitdev->ref[index].freq = curr_freq;
+ else
+ sitdev->out[index].freq = curr_freq;
+ }
+
+ j = 0;
+
+ /* Current frequency first, when known. */
+ if (curr_freq) {
+ struct dpll_pin_frequency f = DPLL_PIN_FREQUENCY(curr_freq);
+
+ ranges[j++] = f;
+ }
+
+ for (i = 0; i < num_freqs; i++) {
+ struct dpll_pin_frequency freq = DPLL_PIN_FREQUENCY(freqs[i]);
+
+ if (freqs[i] == curr_freq)
+ continue;
+ ranges[j++] = freq;
+ }
+
+ if (dir == DPLL_PIN_DIRECTION_OUTPUT && num_freqs == 0) {
+ ranges[j].min = 1;
+ ranges[j].max = 1000000000ULL; /* 1 GHz */
+ j++;
+ }
+
+ if (j > 0) {
+ props->dpll_props.freq_supported = ranges;
+ props->dpll_props.freq_supported_num = j;
+ } else {
+ kfree(ranges);
+ props->dpll_props.freq_supported = NULL;
+ props->dpll_props.freq_supported_num = 0;
+ }
+
+ kfree(freqs);
+
+ return props;
+
+err_alloc_ranges:
+ kfree(freqs);
+err_alloc_freqs:
+ fwnode_handle_put(props->fwnode);
+ kfree(props);
+
+ return ERR_PTR(rc);
+}
+
+/*
+ * sit9531x_pin_props_put - release pin properties
+ * @props: pin properties to free
+ */
+void sit9531x_pin_props_put(struct sit9531x_pin_props *props)
+{
+ kfree(props->dpll_props.freq_supported);
+
+ if (props->fwnode)
+ fwnode_handle_put(props->fwnode);
+
+ kfree(props);
+}
+
+/*
+ * sit9531x_prop_dpll_type_get - get DPLL channel type from firmware
+ * @index: DPLL channel index (0-3)
+ *
+ * Reads the "dpll-types" string array property from the firmware node
+ * and returns the corresponding DPLL type enum.
+ *
+ * Return: DPLL type for the given channel (default: DPLL_TYPE_PPS)
+ */
+enum dpll_type
+sit9531x_prop_dpll_type_get(struct sit9531x_dev *sitdev, u8 index)
+{
+ const char *types[SIT9531X_NUM_PLLS];
+ int count;
+
+ count = device_property_read_string_array(sitdev->dev, "dpll-types",
+ types, ARRAY_SIZE(types));
+
+ if (index >= count)
+ return DPLL_TYPE_PPS;
+
+ if (!strcmp(types[index], "pps"))
+ return DPLL_TYPE_PPS;
+ else if (!strcmp(types[index], "eec"))
+ return DPLL_TYPE_EEC;
+
+ dev_warn(sitdev->dev, "Unknown DPLL type '%s', using default\n",
+ types[index]);
+
+ return DPLL_TYPE_PPS;
+}
diff --git a/drivers/dpll/sit9531x/prop.h b/drivers/dpll/sit9531x/prop.h
new file mode 100644
index 000000000000..a65efdfd9ef8
--- /dev/null
+++ b/drivers/dpll/sit9531x/prop.h
@@ -0,0 +1,37 @@
+/* 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")
+ */
+struct sit9531x_pin_props {
+ struct fwnode_handle *fwnode;
+ struct dpll_pin_properties dpll_props;
+ char package_label[8];
+};
+
+enum dpll_type sit9531x_prop_dpll_type_get(struct sit9531x_dev *sitdev,
+ u8 index);
+struct sit9531x_pin_props *sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
+ enum dpll_pin_direction dir,
+ u8 index);
+void sit9531x_pin_props_put(struct sit9531x_pin_props *props);
+
+#endif /* _SIT9531X_PROP_H */
--
2.43.0
^ permalink raw reply [flat|nested] 26+ messages in thread* Re: [PATCH net-next v11 05/13] dpll: sit9531x: read DPLL types and pin properties from system firmware
2026-09-30 23:37 ` [PATCH net-next v11 05/13] dpll: sit9531x: read DPLL types and pin properties from system firmware Ali Rouhi
@ 2026-10-05 1:09 ` netdev-bot+sashiko
0 siblings, 0 replies; 26+ messages in thread
From: netdev-bot+sashiko @ 2026-10-05 1:09 UTC (permalink / raw)
To: arouhi
Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba,
pabeni, robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
devicetree, netdev, linux-kernel
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 1 · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] sit9531x_pin_props_get() in drivers/dpll/sit9531x/prop.c takes
the first entry of an input pin's "supported-frequencies-hz" as the…
--
dpll: sit9531x: read DPLL types and pin properties from system firmware
This commit adds prop.c to the sit9531x driver. It builds DPLL pin
properties (package and board labels, pin type, capabilities and supported
frequencies) from the input-pins/output-pins firmware nodes, and uses
defaults when a node is missing. It also reads per-channel DPLL types from a
"dpll-types" property, and adds input/output state arrays and pair/lane
helpers to core.h.
> diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
> new file mode 100644
> index 0000000000000..3635ea497e415
> --- /dev/null
> +++ b/drivers/dpll/sit9531x/prop.c
> @@ -0,0 +1,427 @@
[ ... ]
> +struct sit9531x_pin_props *
> +sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
> + enum dpll_pin_direction dir, u8 index)
> +{
[ ... ]
> + /*
> + * Seed an input's runtime ref->freq with the first DT-listed
> + * supported frequency: for an input the board lists the rate that is
> + * physically wired to it first. An output's current rate is what its
> + * divider produces, not an entry of the list it may be set to.
> + */
> + if (num_freqs > 0 && dir == DPLL_PIN_DIRECTION_INPUT &&
> + index != SIT9531X_MAX_INPUTS)
> + curr_freq = freqs[0];
[Severity: Medium]
Do the bindings define an order for supported-frequencies-hz?
The generic binding in dpll-pin.yaml describes it only as "List of
supported frequencies for this pin, expressed in Hz.". The input-pins
section of sitime,sit95316.yaml documents the reg numbering, but it says
nothing about the first entry being the rate wired to the input. The commit
message doesn't mention this rule either. It only appears in the comment
above.
The value picked here is then cached further down in
sit9531x_pin_props_get():
if (dir == DPLL_PIN_DIRECTION_INPUT)
sitdev->ref[index].freq = curr_freq;
and it is listed first in freq_supported. Later in the series, "dpll:
sit9531x: add support to get and set frequency on pins" makes
sit9531x_dpll_input_pin_frequency_get() report it directly:
ref = sit9531x_ref_state_get(sitdpll->dev, dpin->id);
*frequency = ref->freq;
Input pins have no frequency setter that could correct it. That commit's
message says only that the rate is "reported from the firmware
description".
Suppose a board DT lists an input's frequencies in some other order, for
example ascending. It would pass dt_binding_check, but the driver would
report an input frequency over DPLL netlink that the board does not
present.
Since this ordering would become de facto DT ABI once boards ship, could the
binding document it? Two other options: carry the wired rate in a separate
property, or stop inferring the current rate from its position in the list.
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260930233714.87679-1-arouhi%40sitime.com
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH net-next v11 06/13] dpll: sit9531x: register DPLL devices and pins
2026-09-30 23:37 [PATCH net-next v11 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (4 preceding siblings ...)
2026-09-30 23:37 ` [PATCH net-next v11 05/13] dpll: sit9531x: read DPLL types and pin properties from system firmware Ali Rouhi
@ 2026-09-30 23:37 ` Ali Rouhi
2026-10-05 1:09 ` netdev-bot+sashiko
2026-09-30 23:37 ` [PATCH net-next v11 07/13] dpll: sit9531x: implement input pin state on a DPLL Ali Rouhi
` (6 subsequent siblings)
12 siblings, 1 reply; 26+ messages in thread
From: Ali Rouhi @ 2026-09-30 23:37 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi, devicetree,
netdev, linux-kernel
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Register each of the four on-chip PLLs as a DPLL device, and the crystal
that feeds them as a pin on each. The inputs and the outputs need a
state callback the core insists on before it will take a pin, so they are
registered by the patches that add theirs; what this patch builds is the
device, the pin machinery around it, and the one pin whose state never
changes.
The callbacks a device needs from the start come with it -- lock status
and the operating mode -- along with the direction of each pin. The rest
of the pin operations follow in later patches. The device selects its
reference by priority, so automatic is the one mode advertised. A
profile that sets manual active select is still reported as automatic,
since nothing in the driver drives a manual selection, and probe warns
about it.
Both need somewhere to read from. The chip reports its state across
several pages, and a netlink call must not turn into a burst of I2C
transactions, so a kthread worker polls the interesting registers twice a
second into a per-input and per-PLL cache, and the callbacks answer from
it. Output state is not polled: it changes only through a request, and
the getter reads the device again when one could not be confirmed. The
same tick compares the new state against the old and notifies the core
only on a change; the first tick takes the baseline. Where a board wires
INTRB, the interrupt schedules that tick immediately rather than
replacing it: the poll stays as the fallback, since this device gives no
way to be sure an edge was not missed.
Which PLL drives an output is read from the PLLs' output-enable masks,
which PLLC and PLLD keep in mirrored bit order. An output two masks
claim goes to the first PLL, with a warning.
The clock_id that keys a device in the DPLL core is built from the SiTime
OUI and the part's position: sixteen bits of the I2C bus number and the
address. Bus numbers are not bounded by the number of adapters -- a DT
alias or a mux child can number a bus 256 or above on a board with a
handful of them -- so the bus gets the width; the variant needs no field,
since one address on one bus holds one part.
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
drivers/dpll/sit9531x/Kconfig | 4 +
drivers/dpll/sit9531x/Makefile | 2 +-
drivers/dpll/sit9531x/core.c | 1761 ++++++++++++++++++++++++++++++--
drivers/dpll/sit9531x/core.h | 258 ++++-
drivers/dpll/sit9531x/dpll.c | 389 +++++++
drivers/dpll/sit9531x/dpll.h | 67 ++
drivers/dpll/sit9531x/regs.h | 259 ++++-
7 files changed, 2646 insertions(+), 94 deletions(-)
create mode 100644 drivers/dpll/sit9531x/dpll.c
create mode 100644 drivers/dpll/sit9531x/dpll.h
diff --git a/drivers/dpll/sit9531x/Kconfig b/drivers/dpll/sit9531x/Kconfig
index ac5b7f31a034..47aea8674327 100644
--- a/drivers/dpll/sit9531x/Kconfig
+++ b/drivers/dpll/sit9531x/Kconfig
@@ -9,5 +9,9 @@ config SIT9531X_DPLL
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
index 819af61123f5..b97d2656a460 100644
--- a/drivers/dpll/sit9531x/Makefile
+++ b/drivers/dpll/sit9531x/Makefile
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_SIT9531X_DPLL) += sit9531x.o
-sit9531x-y := core.o prop.o
+sit9531x-y := core.o dpll.o prop.o
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index c7c14442df92..bf6817b9d565 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -6,9 +6,11 @@
* Author: Ali Rouhi <arouhi@sitime.com>
* Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
*
- * I2C probe, paged regmap configuration and register access helpers.
+ * Core I2C probe, regmap configuration, hardware state management,
+ * and periodic work thread.
*/
+#include <linux/bitfield.h>
#include <linux/bits.h>
#include <linux/clk.h>
#include <linux/delay.h>
@@ -16,14 +18,31 @@
#include <linux/device.h>
#include <linux/gpio/consumer.h>
#include <linux/i2c.h>
+#include <linux/interrupt.h>
#include <linux/kernel.h>
+#include <linux/kthread.h>
#include <linux/module.h>
+#include <linux/pm.h>
#include <linux/property.h>
#include <linux/regmap.h>
+#include <linux/slab.h>
+#include <linux/string.h>
#include "core.h"
+#include "dpll.h"
+#include "prop.h"
#include "regs.h"
+/*
+ * Number of input + output pin positions for pin index allocation. The two
+ * extra input positions are the crystal and the INTSYNC destination, the
+ * extra output position is the INTSYNC source.
+ */
+#define SIT9531X_NUM_INPUT_PINS (SIT9531X_MAX_INPUTS + 2)
+#define SIT9531X_NUM_OUTPUT_PINS (SIT9531X_MAX_OUTPUTS + 1)
+#define SIT9531X_NUM_PINS_TOTAL (SIT9531X_NUM_INPUT_PINS + \
+ SIT9531X_NUM_OUTPUT_PINS)
+
#define SIT9531X_CHIP(_id, _nin, _nout, _name, _map) \
{ .id = (_id), .num_inputs = (_nin), .num_outputs = (_nout), \
.name = (_name), .clkout_map = (_map) }
@@ -50,8 +69,9 @@ static const struct sit9531x_chip_info sit9531x_chip_ids[] = {
*
* The cached value is only as good as the last transfer, though. A
* transfer that fails can leave the part on another page than the cache
- * records, so every accessor drops the entry on an error and the next
- * access reads the selector again.
+ * records, and a part that lost power across suspend is back on page 0.
+ * Every accessor drops the entry on an error and resume drops it too, so
+ * the next access reads the selector again.
*/
static bool sit9531x_volatile_reg(struct device *dev __maybe_unused,
unsigned int reg)
@@ -193,109 +213,1673 @@ int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
return rc;
}
-static int sit9531x_read_variant_id(struct sit9531x_dev *sitdev, u8 *id)
+/*
+ * sit9531x_input_get_regs - get force mask and state register addresses
+ * @index: logical input index
+ * @force_reg: output force mask register address
+ * @state_reg: output state register address
+ *
+ * Selects the correct Page 0x02 register pair based on the pair's
+ * signal mode and the lane (P/N) the index refers to.
+ */
+static void sit9531x_input_get_regs(const struct sit9531x_dev *sitdev,
+ u8 index,
+ unsigned int *force_reg,
+ unsigned int *state_reg)
{
- return sit9531x_read_u8(sitdev, SIT9531X_REG_VARIANT_ID, id);
+ if (sitdev->ref[index].sig_mode == SIT9531X_MODE_DE) {
+ *force_reg = SIT9531X_REG_IN_DE_FORCE;
+ *state_reg = SIT9531X_REG_IN_DE_STATE;
+ } else if (sit9531x_input_is_n(index)) {
+ *force_reg = SIT9531X_REG_IN_SEN_FORCE;
+ *state_reg = SIT9531X_REG_IN_SEN_STATE;
+ } else {
+ *force_reg = SIT9531X_REG_IN_SEP_FORCE;
+ *state_reg = SIT9531X_REG_IN_SEP_STATE;
+ }
}
-static const struct sit9531x_chip_info *sit9531x_match_variant(u8 id)
+/*
+ * Output enable / disable (Hi-Z control)
+ *
+ * SiT9531x outputs can be configured as differential (DIFF) or
+ * single-ended (CMOS) depending on the factory blob. Each output slot
+ * has THREE Hi-Z force/state register pairs on Page 0x03: one for the
+ * differential path and one for each CMOS pad, OutP and OutN.
+ *
+ * A mute writes all three so the output goes quiet however it is wired.
+ * That includes both CMOS pads, as SiTime's Output_Disable_Control_HiZ.py
+ * does: the register map names bit 3 of ODRn_MISC0 CMOS_ENP and bit 2
+ * CMOS_ENN, while its table of values gives 0100 as CMOS on OutP, so the
+ * one pad a single-ended configuration drives cannot be told from it.
+ *
+ * slot 0-7 :
+ * DIFF 0xF2/0xF3 CMOS OutP 0xF8/0xF9 CMOS OutN 0xF6/0xF7
+ * slot 8-11:
+ * DIFF 0xF4/0xF5 bits 3:0 CMOS OutP 0xFA/0xFB
+ * CMOS OutN 0xF4/0xF5 bits 7:4
+ *
+ * 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[].
+ */
+
+enum {
+ SIT9531X_HIZ_DIFF,
+ SIT9531X_HIZ_CMOS_P,
+ SIT9531X_HIZ_CMOS_N,
+ SIT9531X_HIZ_PAIRS,
+};
+
+struct sit9531x_hiz_pair {
+ unsigned int mask;
+ unsigned int state;
+ u8 bit;
+};
+
+static void sit9531x_output_get_hiz_regs(u8 slot,
+ struct sit9531x_hiz_pair *p)
{
- unsigned int i;
+ struct sit9531x_hiz_pair *diff = &p[SIT9531X_HIZ_DIFF];
+ struct sit9531x_hiz_pair *cmos_p = &p[SIT9531X_HIZ_CMOS_P];
+ struct sit9531x_hiz_pair *cmos_n = &p[SIT9531X_HIZ_CMOS_N];
- for (i = 0; i < ARRAY_SIZE(sit9531x_chip_ids); i++) {
- if (sit9531x_chip_ids[i].id == id)
- return &sit9531x_chip_ids[i];
+ if (slot <= 7) {
+ diff->mask = SIT9531X_REG_HIZ_DIFF_07_MASK;
+ diff->state = SIT9531X_REG_HIZ_DIFF_07_STATE;
+ diff->bit = slot;
+ cmos_p->mask = SIT9531X_REG_HIZ_SE_07_MASK;
+ cmos_p->state = SIT9531X_REG_HIZ_SE_07_STATE;
+ cmos_p->bit = slot;
+ cmos_n->mask = SIT9531X_REG_HIZ_SEN_07_MASK;
+ cmos_n->state = SIT9531X_REG_HIZ_SEN_07_STATE;
+ cmos_n->bit = slot;
+ } else {
+ diff->mask = SIT9531X_REG_HIZ_DIFF_811_MASK;
+ diff->state = SIT9531X_REG_HIZ_DIFF_811_STATE;
+ diff->bit = slot - 8;
+ cmos_p->mask = SIT9531X_REG_HIZ_SE_811_MASK;
+ cmos_p->state = SIT9531X_REG_HIZ_SE_811_STATE;
+ cmos_p->bit = slot - 8;
+ cmos_n->mask = SIT9531X_REG_HIZ_DIFF_811_MASK;
+ cmos_n->state = SIT9531X_REG_HIZ_DIFF_811_STATE;
+ cmos_n->bit = slot - 8 + SIT9531X_HIZ_SEN_811_SHIFT;
}
+}
- return NULL;
+/*
+ * sit9531x_output_mode_fetch - read how an output is wired
+ *
+ * The Hi-Z force is a separate register pair for the differential path
+ * and for each CMOS pad, and only the pairs belonging to the way the
+ * output is actually wired say anything about whether it is quiet. The
+ * others hold whatever the loaded configuration left in them.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_output_mode_fetch(struct sit9531x_dev *sitdev, u8 out_idx)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ u8 slot, page, reg, val;
+ int rc;
+
+ slot = info->clkout_map[out_idx];
+ page = (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX) ?
+ SIT9531X_PAGE_OUTSYS1 : SIT9531X_PAGE_OUTSYS0;
+ reg = SIT9531X_OUT_MISC0_BASE +
+ SIT9531X_OUT_MISC0_STRIDE * (slot % 6);
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG(page, reg), &val);
+ if (rc)
+ return rc;
+
+ sitdev->out[out_idx].cmos = !!(val & (SIT9531X_OUT_CMOS_ENP |
+ SIT9531X_OUT_CMOS_ENN));
+
+ return 0;
}
-int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
+static int sit9531x_hiz_pair_forced(struct sit9531x_dev *sitdev,
+ const struct sit9531x_hiz_pair *p,
+ bool *forced)
{
- struct clk *xtal_clk;
- u8 variant_id;
+ u8 mask, state;
+ int rc;
+
+ rc = sit9531x_read_u8(sitdev, p->mask, &mask);
+ if (rc)
+ return rc;
+ rc = sit9531x_read_u8(sitdev, p->state, &state);
+ if (rc)
+ return rc;
+
+ *forced = (mask & BIT(p->bit)) && !(state & BIT(p->bit));
+
+ return 0;
+}
+
+/*
+ * Report whether a slot is currently forced into Hi-Z, i.e. the driver
+ * (or the loaded profile) took control of the pin (MASK bit set) and
+ * forces it to Hi-Z (STATE bit clear). Only the pairs that match how
+ * the output is wired are read: that is what controls the pad. A CMOS
+ * output reads as muted only when both of its pads are forced, since
+ * which one the configuration drives cannot be told (see above).
+ */
+static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev,
+ u8 out_idx, bool *muted)
+{
+ struct sit9531x_hiz_pair p[SIT9531X_HIZ_PAIRS];
int rc;
+ sit9531x_output_get_hiz_regs(sitdev->info->clkout_map[out_idx], p);
+
/*
- * Fvco = Fref * (DIVN + frac/2^32) with Fref derived from the XO
- * feeding XIN/XO_CLK, so the rate is needed before anything can be
- * computed from a divider. The rate normally comes from a "clocks"
- * phandle (clock-names = "xtal"). On platforms where the firmware
- * does not expose the XO through the clock framework, fall back to
- * a "clock-frequency" device property.
+ * Testing a pair the output is not wired through and taking it as
+ * proof of a mute answers from a register nothing drives, and
+ * disagrees with itself when a mute lands on one pair and fails
+ * on another.
*/
- xtal_clk = devm_clk_get_optional_enabled(sitdev->dev, "xtal");
- if (IS_ERR(xtal_clk))
- return dev_err_probe(sitdev->dev, PTR_ERR(xtal_clk),
- "Failed to get xtal clock\n");
- sitdev->xtal_freq = clk_get_rate(xtal_clk);
- if (!sitdev->xtal_freq) {
- u32 freq;
+ if (!sitdev->out[out_idx].cmos)
+ return sit9531x_hiz_pair_forced(sitdev,
+ &p[SIT9531X_HIZ_DIFF], muted);
- if (!device_property_read_u32(sitdev->dev, "clock-frequency",
- &freq))
- sitdev->xtal_freq = freq;
+ rc = sit9531x_hiz_pair_forced(sitdev, &p[SIT9531X_HIZ_CMOS_P], muted);
+ if (rc || !*muted)
+ return rc;
+
+ return sit9531x_hiz_pair_forced(sitdev, &p[SIT9531X_HIZ_CMOS_N],
+ muted);
+}
+
+/*
+ * Input priority selection
+ *
+ * The SiT9531x has an 11-slot priority table per PLL on Page 1. Each
+ * register holds two slots nibble-packed: the earlier (even, 2n) slot
+ * in [7:4] and the later (odd, 2n+1) slot in [3:0].
+ *
+ * The procedure:
+ * 1. Force PLL into holdover (PLL page reg 0x6F bit 4)
+ * 2. Write priority slots on Page 1
+ * 3. Small change update (Page 0 reg 0x0F bit 1)
+ * 4. Release holdover
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+
+/* Page-1 register holding priority slot @slot of @pll_idx. */
+static u16 sit9531x_prio_reg(u8 pll_idx, u8 slot)
+{
+ return SIT9531X_REG(SIT9531X_PAGE_PRIOSYS,
+ SIT9531X_PRIO_BASE_REG +
+ SIT9531X_PRIO_REGS_PER_PLL * pll_idx +
+ slot / SIT9531X_PRIO_SLOTS_PER_REG);
+}
+
+/*
+ * Extract priority slot @slot from its register value. The register
+ * holding slots 2n and 2n+1 keeps the earlier slot in the high nibble
+ * (CLK_SPARE<2n>SEL) and the later one in the low nibble.
+ */
+static u8 sit9531x_prio_slot_get(u8 val, u8 slot)
+{
+ if (slot & 1)
+ return val & SIT9531X_PRIO_NIBBLE_MASK;
+
+ return val >> SIT9531X_PRIO_HI_SHIFT;
+}
+
+/*
+ * Rebuild a PLL's membership mask from the source codes of its priority
+ * table. The mask is what the pin state getters test, so it is refreshed
+ * from exactly the values the table holds -- here after a write, and once
+ * per poll from the read-back in sit9531x_chan_state_fetch().
+ */
+static void sit9531x_prio_mask_build(struct sit9531x_dev *sitdev, u8 pll_idx,
+ const u8 *srcs)
+{
+ u16 mask = 0;
+ u8 slot;
+
+ for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+ u8 src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
+
+ if (sit9531x_prio_src_usable(src))
+ mask |= BIT(src);
}
- 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");
+
+ sitdev->chan[pll_idx].prio_mask = mask;
+}
+
+/*
+ * sit9531x_prio_table_read - read a PLL's priority-table source codes
+ * @srcs: output array of SIT9531X_PRIO_MAX_SLOTS source codes
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_prio_table_read(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 *srcs)
+{
+ u8 val, slot;
+ int rc;
+
+ for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+ rc = sit9531x_read_u8(sitdev,
+ sit9531x_prio_reg(pll_idx, slot), &val);
+ if (rc)
+ return rc;
+
+ srcs[slot] = sit9531x_prio_slot_get(val, slot);
+ }
+
+ return 0;
+}
+
+/* XO doubler register */
+#define SIT9531X_REG_XO2_GENERIC SIT9531X_REG(0x00, 0x2D)
+#define SIT9531X_XO_DOUBLER_ENB_BIT 7 /* inverted: 0 = enabled */
+
+/* VCO frequency bands (Hz) */
+#define SIT9531X_FVCO_LOWBAND_MIN 4915200000ULL
+#define SIT9531X_FVCO_LOWBAND_MAX 5898240000ULL
+#define SIT9531X_FVCO_HIGHBAND_MIN 6875000000ULL
+#define SIT9531X_FVCO_HIGHBAND_MAX 7812500000ULL
+
+/* The output divider is a 34-bit field */
+#define SIT9531X_DIVO_MAX GENMASK_ULL(33, 0)
+
+/*
+ * Phase adjust (PRG_RST_DELAY register-based).
+ *
+ * The chip exposes a per-output 34-bit coarse delay measured in VCO
+ * clock periods plus a 3-bit fine delay in fixed 30 ps steps. The
+ * five bytes PROG6..PROG2 hold the field across registers:
+ * base + 0 PROG6 [7:5] OPSTG_VCASC_BUMP (preserved via RMW)
+ * [4:2] PRG_RST_FINE_DELAY
+ * [1:0] PRG_RST_DELAY[33:32]
+ * base + 1 PROG5 PRG_RST_DELAY[31:24]
+ * base + 2 PROG4 PRG_RST_DELAY[23:16]
+ * base + 3 PROG3 PRG_RST_DELAY[15:8]
+ * base + 4 PROG2 PRG_RST_DELAY[7:0]
+ *
+ * Outputs 0-5 live on Page 3, outputs 6-11 on Page 4, with each
+ * output's block at base = 0x15 + 16 * (out_idx % 6).
+ *
+ * The chip only supports unsigned positive delay. A negative phase
+ * adjustment (advance) is wrapped to (T_out - |phase|) modulo one
+ * output period, which is identical for a periodic signal.
+ */
+
+/*
+ * sit9531x_clear_notifications - clear all notification registers
+ *
+ * Clears all write-1-to-clear notification registers:
+ * - PLL outer LOL notification (Page 0, reg 0x07)
+ * - PLL holdover freeze notification (Page 0, reg 0x0B)
+ * - PLL inner LOL notification (Page 0, reg 0x93)
+ * - Clock monitor XO/PLL notification (Page 0, reg 0x9E)
+ * - Clock input notifications (Page 6, regs 0x03/0x07/0x93/0x97)
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
+{
+ int rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ /* Page 0x00 W1C notification registers */
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_OUTER_LOL_NOTIF, 0xFF);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_HO_FREEZE_NOTIF, 0xFF);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PLL_INNER_LOL_NOTIF, 0xFF);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_CMON_NOTIF, 0xFF);
+ if (rc)
+ return rc;
+
+ /* Page 0x06 clock input monitor notifications */
+ rc = sit9531x_write_u8(sitdev, SIT9531X_CLKMON_P_NOTIF_01, 0xFF);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_CLKMON_P_NOTIF_23, 0xFF);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_CLKMON_N_NOTIF_01, 0xFF);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_CLKMON_N_NOTIF_23, 0xFF);
+ if (rc)
+ return rc;
+
+ dev_dbg(sitdev->dev, "All notification registers cleared\n");
+ return 0;
+}
+
+/*
+ * sit9531x_ref_state_fetch - read input reference status from hardware
+ * @index: logical input index
+ *
+ * Reads whether the lane's receiver is on, from the Page 0x02 force and
+ * state bits.
+ */
+static int sit9531x_ref_state_fetch(struct sit9531x_dev *sitdev, u8 index)
+{
+ unsigned int force_reg, state_reg;
+ u8 pair, force, state;
+ struct sit9531x_ref *ref;
+ int rc;
/*
- * Held deasserted, never pulsed: the chip configuration comes from
- * efuse or an NVM blob applied before probe, and a reset would
- * discard it. Must precede the first I2C access, as a board that
- * powers up asserted keeps the chip unreachable until released.
+ * The XTAL/XO reference (index SIT9531X_MAX_INPUTS) is the on-chip
+ * oscillator that feeds every PLL. It cannot be routed or deselected,
+ * so its pin is modeled as permanently connected (see
+ * sit9531x_dpll_xo_pin_ops) and has no receiver to gate. Only the
+ * routable per-lane inputs (0..num_inputs-1) are polled here.
*/
- sitdev->reset_gpio = devm_gpiod_get_optional(sitdev->dev, "reset",
- GPIOD_OUT_LOW);
- if (IS_ERR(sitdev->reset_gpio))
- return dev_err_probe(sitdev->dev, PTR_ERR(sitdev->reset_gpio),
- "Failed to request reset gpio\n");
- if (sitdev->reset_gpio)
- fsleep(10000); /* internal boot after release */
+ if (index >= SIT9531X_MAX_INPUTS)
+ return -EINVAL;
- rc = sit9531x_read_variant_id(sitdev, &variant_id);
+ ref = &sitdev->ref[index];
+ pair = sit9531x_input_pair(index);
+
+ /*
+ * Whether the receiver is on. This has to come from the chip: it
+ * is the loaded configuration that decides, and without reading it
+ * back every input would look disabled until something called
+ * sit9531x_input_enable(). A lane counts as disabled only while
+ * the force bit overrides it to the off state; with the force bit
+ * clear it follows the configuration, which is the enabled case.
+ */
+ sit9531x_input_get_regs(sitdev, index, &force_reg, &state_reg);
+
+ rc = sit9531x_read_u8(sitdev, force_reg, &force);
+ if (rc)
+ return rc;
+ rc = sit9531x_read_u8(sitdev, state_reg, &state);
if (rc)
return rc;
- sitdev->info = sit9531x_match_variant(variant_id);
- if (!sitdev->info)
- return dev_err_probe(sitdev->dev, -ENODEV,
- "Unknown variant ID: 0x%02x\n",
- variant_id);
+ ref->enabled = !((force & BIT(pair)) && !(state & BIT(pair)));
- rc = devm_mutex_init(sitdev->dev, &sitdev->multiop_lock);
+ return 0;
+}
+
+/*
+ * sit9531x_input_mode_fetch - detect SE/DE configuration of an input pair
+ * @pair: input pair number (0-3)
+ *
+ * Reads CLKINx_INPUT_MODE and stores the detected signal mode on both
+ * lanes of the pair. A pair with neither SE lane enabled is running
+ * differential.
+ */
+static int sit9531x_input_mode_fetch(struct sit9531x_dev *sitdev, u8 pair)
+{
+ enum sit9531x_signal_mode sig_mode;
+ u8 mode;
+ int rc;
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_IN_MODE(pair), &mode);
if (rc)
- return dev_err_probe(sitdev->dev, rc,
- "Failed to initialize mutex\n");
+ return rc;
- dev_info(sitdev->dev, "%s detected, %u inputs, %u outputs\n",
- sitdev->info->name, sitdev->info->num_inputs,
- sitdev->info->num_outputs);
+ 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;
}
-static int sit9531x_i2c_probe(struct i2c_client *client)
+/* Read the PLL active-state bit (PLL page reg 0x02 bit 0). */
+static int sit9531x_pll_is_active(struct sit9531x_dev *sitdev, u8 pll_idx,
+ bool *active)
{
- struct sit9531x_dev *sitdev;
- struct regmap *regmap;
+ u8 v;
+ int rc;
- regmap = devm_regmap_init_i2c(client, &sit9531x_regmap_config);
- if (IS_ERR(regmap))
- return dev_err_probe(&client->dev, PTR_ERR(regmap),
- "Failed to initialize regmap\n");
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_ACTIVE, &v);
+ if (rc)
+ return rc;
- sitdev = devm_kzalloc(&client->dev, sizeof(*sitdev), GFP_KERNEL);
- if (!sitdev)
- return -ENOMEM;
+ *active = !!(v & SIT9531X_PLL_ACTIVE_BIT);
- sitdev->dev = &client->dev;
- sitdev->client = client;
- sitdev->regmap = regmap;
- i2c_set_clientdata(client, sitdev);
+ return 0;
+}
- return sit9531x_dev_probe(sitdev);
+/*
+ * sit9531x_chan_state_fetch - read PLL channel status from hardware
+ *
+ * Reads whether the PLL runs, its lock and holdover state, its mode, the
+ * active selection and its priority table.
+ */
+static int sit9531x_chan_state_fetch(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+ u8 status, outer_lol, input_sel, inner_lol, ho_freeze, activesel_reg;
+ struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+ u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+ u8 pll_status_1;
+ bool active;
+ int rc;
+
+ /*
+ * Whether the PLL is running at all. The loss-of-lock bit read
+ * below is driven by the PLL itself, so on one the loaded
+ * configuration leaves unused it simply stays clear and would
+ * otherwise read as a lock.
+ */
+ rc = sit9531x_pll_is_active(sitdev, pll_idx, &active);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_STATUS, &status);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_OUTER_LOL_STATUS,
+ &outer_lol);
+ if (rc)
+ return rc;
+
+ /*
+ * Read the input source the PLL has currently selected as its
+ * active reference. This lives in the low nibble of the last
+ * register of the PLL's page-1 priority block (CLK_ACTIVESEL_PLL),
+ * not on the PLL page -- PLL-page 0x29 is a config register.
+ */
+ activesel_reg = SIT9531X_PRIO_BASE_REG +
+ SIT9531X_PRIO_REGS_PER_PLL * pll_idx +
+ SIT9531X_PRIO_ACTIVESEL_OFF;
+ rc = sit9531x_read_u8(sitdev,
+ SIT9531X_REG(SIT9531X_PAGE_PRIOSYS,
+ activesel_reg),
+ &input_sel);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_PLL_INNER_LOL_STATUS,
+ &inner_lol);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_HO_FREEZE_STATUS,
+ &ho_freeze);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_STATUS_1,
+ &pll_status_1);
+ if (rc)
+ return rc;
+
+ /*
+ * Which sources this PLL may select. The table is configuration and
+ * changes only through the driver, but reading it back keeps the
+ * membership the pin state getters report tied to the hardware
+ * instead of to a value the driver maintains on the side.
+ */
+ rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+ if (rc)
+ return rc;
+
+ sit9531x_prio_mask_build(sitdev, pll_idx, srcs);
+
+ /* STATUS_1_GENERIC reports loss of lock, so invert it. */
+ chan->active = active;
+ chan->locked = active && !(outer_lol & BIT(pll_idx));
+ chan->mode = !!(status & SIT9531X_PLL_STATUS_OUTER_DIS);
+ chan->selected_ref =
+ sit9531x_hw_src_input(input_sel & SIT9531X_PRIO_NIBBLE_MASK);
+ chan->inner_lol = !!(inner_lol & BIT(pll_idx));
+ chan->ho_freeze = !!(ho_freeze & BIT(pll_idx));
+ chan->ho_valid = !!(pll_status_1 & SIT9531X_PLL_STATUS_1_HO_VALID);
+
+ return 0;
+}
+
+/*
+ * sit9531x_out_state_fetch - read output status from hardware
+ *
+ * Takes the output PLL association from the PLL page output map
+ * registers into out->routed / out->pll_idx, and the current drive state
+ * from the Hi-Z force bits into out->enabled. The two are separate:
+ * routing decides whether the output gets a DPLL pin at all,
+ * while a muted but routed output keeps its pin and reports
+ * DPLL_PIN_STATE_DISCONNECTED until it is un-muted.
+ */
+static int sit9531x_out_state_fetch(struct sit9531x_dev *sitdev, u8 index)
+{
+ struct sit9531x_out *out = &sitdev->out[index];
+ u8 map_lo, map_hi, slot, bit, claimed;
+ int pll_idx;
+ u16 mask;
+ bool muted;
+ int rc;
+
+ slot = sitdev->info->clkout_map[index];
+
+ rc = sit9531x_output_mode_fetch(sitdev, index);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_output_forced_hiz(sitdev, index, &muted);
+ if (rc)
+ return rc;
+
+ /*
+ * Each PLL page holds the PLL's output-enable mask, twelve bits:
+ * bits 0-7 in OUT_MAP_LO and bits 8-11 in OUT_MAP_HI[3:0]. PLLA and
+ * PLLB keep OUTn at bit n; PLLC and PLLD keep the outputs in mirrored
+ * order, OUTn at bit 11 - n, as SiTime's procedure scripts read
+ * them. The index is the physical output the pin drives, not
+ * the driver's logical one (translated above via the chip-info
+ * clkout_map[]: identity on SiT95316, non-contiguous on SiT95317).
+ *
+ * An output belongs to one PLL. Take the first that claims it, and
+ * say so if another does too.
+ */
+ claimed = 0;
+ 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;
+
+ mask = map_lo | (map_hi & GENMASK(3, 0)) << 8;
+ bit = pll_idx < 2 ? slot : SIT9531X_MAX_OUTPUTS - 1 - slot;
+ if (mask & BIT(bit))
+ claimed |= BIT(pll_idx);
+ }
+
+ if (!claimed) {
+ /* Output not mapped to any PLL */
+ out->pll_idx = 0;
+ out->routed = false;
+ out->enabled = false;
+ return 0;
+ }
+
+ out->pll_idx = __ffs(claimed);
+ out->routed = true;
+ out->enabled = !muted;
+
+ if (claimed & (claimed - 1))
+ dev_warn(sitdev->dev,
+ "OUT%u is in the output-enable mask of more than one PLL (0x%x); taking PLL%c\n",
+ slot, claimed, 'A' + out->pll_idx);
+
+ return 0;
+}
+
+/*
+ * sit9531x_ref_pll_mask_fetch - seed the input-to-PLL usage masks
+ *
+ * ref->pll_mask is the refcount the disconnect path uses to decide when
+ * an input receiver may be powered down: the physical input is only
+ * disabled once the last DPLL has released it. It therefore has to
+ * start out matching the hardware. Without this pass every mask starts
+ * at zero, and disconnecting an input from one DPLL drops the mask to
+ * zero and disables a receiver the other DPLLs are still locked to.
+ *
+ * An input is counted for a PLL when it appears in that PLL's Page-1
+ * priority table, which is exactly the condition the connect and
+ * disconnect callbacks maintain. Sources that are not physical inputs
+ * (OCXO, INTSYNC) and reserved codes are skipped.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_ref_pll_mask_fetch(struct sit9531x_dev *sitdev)
+{
+ u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+ u8 pll_idx, slot, index;
+ int rc;
+
+ for (pll_idx = 0; pll_idx < SIT9531X_NUM_PLLS; pll_idx++) {
+ rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+ if (rc)
+ return rc;
+
+ for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+ index = sit9531x_hw_src_input(srcs[slot]);
+ if (index >= sitdev->info->num_inputs)
+ continue;
+
+ /*
+ * On a differentially configured pair only the P lane
+ * has a DPLL pin, so that is the lane the connect and
+ * disconnect callbacks account for. Fold an N-lane
+ * table entry onto its P lane, otherwise the count
+ * would land on a lane nothing ever releases. The
+ * signaling mode is already known here:
+ * sit9531x_input_mode_fetch() runs first.
+ */
+ if (sit9531x_input_is_n(index) &&
+ sitdev->ref[index].sig_mode == SIT9531X_MODE_DE)
+ index--;
+
+ sitdev->ref[index].pll_mask |= BIT(pll_idx);
+ }
+ }
+
+ return 0;
+}
+
+/*
+ * sit9531x_dev_state_fetch - read all hardware state at startup
+ *
+ * Called once during probe to populate the initial state cache.
+ */
+static int sit9531x_dev_state_fetch(struct sit9531x_dev *sitdev)
+{
+ int rc;
+ u8 i;
+
+ /* Detect SE/DE configuration before any per-lane access */
+ for (i = 0; i < sitdev->info->num_inputs / 2; i++) {
+ rc = sit9531x_input_mode_fetch(sitdev, i);
+ if (rc) {
+ dev_err(sitdev->dev,
+ "Failed to fetch CLKIN%u mode: %d\n", i, rc);
+ return rc;
+ }
+ }
+
+ for (i = 0; i < sitdev->info->num_inputs; i++) {
+ rc = sit9531x_ref_state_fetch(sitdev, i);
+ if (rc) {
+ dev_err(sitdev->dev,
+ "Failed to fetch input %u state: %d\n", i, rc);
+ return rc;
+ }
+ }
+
+ /*
+ * The priority-table read walks the Page-1 registers, so it runs
+ * with multiop_lock held like every other multi-register sequence.
+ * Nothing can race with it here -- the DPLLs are not registered and
+ * the monitor is not running yet -- but the page handling stays
+ * serialized the same way as at runtime.
+ */
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_ref_pll_mask_fetch(sitdev);
+ mutex_unlock(&sitdev->multiop_lock);
+ if (rc) {
+ dev_err(sitdev->dev,
+ "Failed to fetch input priority tables: %d\n", rc);
+ return rc;
+ }
+
+ for (i = 0; i < sitdev->info->num_outputs; i++) {
+ rc = sit9531x_out_state_fetch(sitdev, i);
+ if (rc) {
+ dev_err(sitdev->dev,
+ "Failed to fetch output %u state: %d\n", i, rc);
+ return rc;
+ }
+ }
+
+ for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+ rc = sit9531x_chan_state_fetch(sitdev, i);
+ if (rc) {
+ dev_err(sitdev->dev,
+ "Failed to fetch PLL%c state: %d\n",
+ 'A' + i, rc);
+ return rc;
+ }
+ }
+
+ return 0;
+}
+
+static void sit9531x_dev_ref_states_update(struct sit9531x_dev *sitdev)
+{
+ int i, rc;
+
+ for (i = 0; i < sitdev->info->num_inputs; i++) {
+ rc = sit9531x_ref_state_fetch(sitdev, i);
+ if (rc)
+ dev_warn(sitdev->dev,
+ "Failed to get REF%u status: %d\n", i, rc);
+ }
+}
+
+static void sit9531x_dev_chan_states_update(struct sit9531x_dev *sitdev)
+{
+ int i, rc;
+
+ for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+ rc = sit9531x_chan_state_fetch(sitdev, i);
+ if (rc)
+ dev_warn(sitdev->dev,
+ "Failed to get PLL%c state: %d\n",
+ 'A' + i, rc);
+ }
+}
+
+/*
+ * sit9531x_dev_periodic_work - periodic hardware state polling
+ * @work: kthread_work pointer
+ *
+ * Polls hardware state at SIT9531X_STATUS_POLL_MS intervals.
+ * Updates reference and channel states, then delegates change
+ * detection to sit9531x_dpll_changes_check() for each registered DPLL.
+ */
+static void sit9531x_dev_periodic_work(struct kthread_work *work)
+{
+ struct sit9531x_dev *sitdev = container_of(work, struct sit9531x_dev,
+ work.work);
+ struct sit9531x_dpll *sitdpll;
+ int rc;
+
+ /*
+ * Update the cached ref[]/chan[] arrays under multiop_lock so the
+ * fetches are serialized against the DPLL callbacks that read
+ * these fields and against the chip's page selector.
+ *
+ * The lock is then dropped before sit9531x_dpll_changes_check(),
+ * which calls dpll_pin_change_ntf() / dpll_device_change_ntf().
+ * Those notification helpers take DPLL-subsystem locks that are
+ * already held when our callbacks are invoked from netlink
+ * context, and nesting multiop_lock around them would invert the
+ * lock order. changes_check() reads the cache published above,
+ * which is already consistent.
+ */
+ mutex_lock(&sitdev->multiop_lock);
+ sit9531x_dev_ref_states_update(sitdev);
+ sit9531x_dev_chan_states_update(sitdev);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ list_for_each_entry(sitdpll, &sitdev->dplls, list)
+ sit9531x_dpll_changes_check(sitdpll);
+
+ /*
+ * Acknowledge the chip's notification latches after the tick has
+ * read and acted on them. Without this, the W1C bits remain set
+ * and -- on boards that wire INTRB -- the line stays asserted,
+ * re-firing the threaded handler back to back. The helper writes
+ * W1C bits across page 0 and page 6 and must run under
+ * multiop_lock to serialize the page selector against userspace
+ * dpll ops. Failure is non-fatal: status was already consumed
+ * for this tick and the next tick re-processes whatever stayed
+ * latched.
+ */
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_clear_notifications(sitdev);
+ mutex_unlock(&sitdev->multiop_lock);
+ if (rc)
+ dev_warn_ratelimited(sitdev->dev,
+ "Failed to clear notifications: %d\n",
+ rc);
+
+ /* Run twice a second */
+ kthread_queue_delayed_work(sitdev->kworker, &sitdev->work,
+ msecs_to_jiffies(SIT9531X_STATUS_POLL_MS));
+}
+
+/*
+ * sit9531x_irq_thread_fn - threaded IRQ handler for the chip's INTRB line
+ *
+ * Triggered when the chip asserts INTRB (and only when DT wires up the
+ * client interrupt; absent property == handler never installed). The
+ * action mirrors a periodic-work tick: queue an immediate run so status
+ * registers are read and DPLL changes_check fires without waiting for
+ * the next poll deadline. Polling continues to run as a fallback.
+ */
+static irqreturn_t sit9531x_irq_thread_fn(int irq, void *data)
+{
+ struct sit9531x_dev *sitdev = data;
+ int rc;
+
+ /*
+ * Acknowledge the chip's notification latches from the threaded
+ * handler itself. With IRQF_ONESHOT the line is unmasked on
+ * return, so deferring the W1C clear to the async kworker would
+ * let a still-asserted INTRB re-fire immediately (interrupt storm).
+ * Clear here, then kick the poll worker to read state and run
+ * changes_check.
+ */
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_clear_notifications(sitdev);
+ mutex_unlock(&sitdev->multiop_lock);
+ if (rc) {
+ dev_warn_ratelimited(sitdev->dev,
+ "IRQ: failed to clear notifications: %d\n",
+ rc);
+ /*
+ * The latch was not acknowledged, so with IRQF_ONESHOT the
+ * still-asserted line re-enters this handler as soon as it
+ * returns. Returning IRQ_NONE leaves that to the spurious
+ * detector, which needs roughly 100000 interrupts and resets
+ * its count every tenth of a second -- unreachable when each
+ * pass costs an I2C timeout. Give up on the line instead:
+ * the periodic poll reads the same state without it, so the
+ * driver keeps working on a board whose INTRB cannot be
+ * acknowledged.
+ */
+ if (++sitdev->irq_ack_fails < SIT9531X_IRQ_ACK_TRIES)
+ return IRQ_NONE;
+
+ dev_err(sitdev->dev,
+ "IRQ %d disabled: notifications cannot be cleared, polling only\n",
+ irq);
+ disable_irq_nosync(irq);
+ return IRQ_NONE;
+ }
+
+ sitdev->irq_ack_fails = 0;
+
+ kthread_mod_delayed_work(sitdev->kworker, &sitdev->work, 0);
+ return IRQ_HANDLED;
+}
+
+/*
+ * The driver reports every PLL as selecting by priority. A profile can
+ * put a PLL in manual active select instead (PLL_CONFIG1F_PLL bit 6), and
+ * the mode userspace sees would then be wrong; say so rather than
+ * reconfigure what the profile chose. The warning also says what the PLL
+ * then follows: its active selection, or with MISCINNER_PLL bit 5 the
+ * manual input select -- the input-select pins, or the input code that
+ * GPIO_INPUT_FUNC_CTRL5..8 holds when its bit 4 is set.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static void sit9531x_manual_sel_report(struct sit9531x_dev *sitdev)
+{
+ u8 cfg, misc, sel, i;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+ if (sit9531x_read_pll_u8(sitdev, i, SIT9531X_PLL_REG_CONFIG1F,
+ &cfg))
+ return;
+ if (!(cfg & SIT9531X_PLL_CONFIG1F_MANUAL_SEL))
+ continue;
+
+ if (sit9531x_read_pll_u8(sitdev, i, SIT9531X_PLL_REG_MISCINNER,
+ &misc))
+ return;
+ if (!(misc & SIT9531X_PLL_MISCINNER_MAN_IN_SEL)) {
+ dev_warn(sitdev->dev,
+ "PLL%c: the profile selects its reference manually, following its active selection; it is reported as automatic\n",
+ 'A' + i);
+ continue;
+ }
+
+ if (sit9531x_read_u8(sitdev, SIT9531X_REG_MAN_IN_SEL(i), &sel))
+ return;
+ if (sel & SIT9531X_MAN_IN_SEL_FROM_REG) {
+ sel = FIELD_GET(SIT9531X_MAN_IN_SEL_MASK, sel);
+ dev_warn(sitdev->dev,
+ "PLL%c: the profile selects its reference manually, input code %u from the register; it is reported as automatic\n",
+ 'A' + i, sel);
+ } else {
+ dev_warn(sitdev->dev,
+ "PLL%c: the profile selects its reference manually, from the input-select pins; it is reported as automatic\n",
+ 'A' + i);
+ }
+ }
+}
+
+/*
+ * Report what the device loaded from its EEPROM, and warn if it does not
+ * look like a healthy load.
+ *
+ * A profile that failed to load leaves the part running something other
+ * than what the board was designed around -- dividers, output routing
+ * and priority tables all differ -- while every register still reads
+ * back a plausible value. Naming the profile and saying whether the
+ * load was clean turns that into something visible at startup instead of
+ * something inferred from measurements later.
+ *
+ * This only reports. Boards in this family may have their
+ * configuration pushed over I2C rather than held in an EEPROM, and there
+ * the CRC pair means nothing, so a mismatch is not grounds for refusing
+ * to drive the device.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static void sit9531x_eeprom_state_report(struct sit9531x_dev *sitdev)
+{
+ u32 rec_crc = 0, cal_crc = 0, prof_id = 0;
+ u8 notif, v;
+ int rc, i;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ /* Profile id: three bytes, least significant first. */
+ for (i = 2; i >= 0; i--) {
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_PROFILE_ID + i, &v);
+ if (rc)
+ return;
+ prof_id = prof_id << 8 | v;
+ }
+
+ dev_info(sitdev->dev, "profile id %u\n", prof_id);
+
+ /* Both CRCs: four bytes, most significant first. */
+ for (i = 0; i < 4; i++) {
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_REC_CRC + i, &v);
+ if (rc)
+ return;
+ rec_crc = rec_crc << 8 | v;
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_CAL_CRC + i, &v);
+ if (rc)
+ return;
+ cal_crc = cal_crc << 8 | v;
+ }
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_EEPROM_NOTIF, ¬if);
+ if (rc)
+ return;
+
+ /*
+ * A clean load leaves the read-done bit set and every defect bit
+ * clear. Both CRCs zero means no EEPROM read happened at all:
+ * boards in this family may take their configuration over I2C
+ * instead of an EEPROM, and there the CRC pair means nothing, so
+ * that case is not a mismatch worth warning about.
+ */
+ if (!rec_crc && !cal_crc)
+ dev_dbg(sitdev->dev,
+ "no EEPROM profile (configuration pushed over I2C)\n");
+ else if (rec_crc != cal_crc)
+ dev_warn(sitdev->dev,
+ "EEPROM CRC mismatch: stored %08x, computed %08x\n",
+ rec_crc, cal_crc);
+ else if (notif != SIT9531X_EEPROM_READ_DONE)
+ dev_warn(sitdev->dev,
+ "EEPROM read reported defects (notify %02x)\n",
+ notif);
+ else
+ dev_dbg(sitdev->dev, "EEPROM profile loaded, CRC %08x\n",
+ cal_crc);
+}
+
+/*
+ * Report which PLLs came up, and flag the one case that is a real
+ * inconsistency rather than a configuration choice.
+ *
+ * A PLL the loaded configuration leaves unused never reaches its active
+ * state, which is normal and not worth a warning. A PLL that has
+ * outputs routed to it and is still not active is different: something
+ * that is meant to be generating clocks is not running, and every value
+ * read from it -- lock state, phase, frequency offset -- describes a
+ * stopped loop. Say so once at startup rather than leaving it to be
+ * discovered through measurements that quietly read as zero.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static void sit9531x_pll_states_report(struct sit9531x_dev *sitdev)
+{
+ unsigned int idx, i;
+ bool routed;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+ if (sitdev->chan[i].active) {
+ dev_dbg(sitdev->dev, "PLL%c active\n", 'A' + i);
+ continue;
+ }
+
+ routed = false;
+ for (idx = 0; idx < sitdev->info->num_outputs; idx++) {
+ const struct sit9531x_out *out;
+
+ out = sit9531x_out_state_get(sitdev, idx);
+ if (out->routed && out->pll_idx == i) {
+ routed = true;
+ break;
+ }
+ }
+
+ if (routed)
+ dev_warn(sitdev->dev,
+ "PLL%c drives outputs but is not in its active state\n",
+ 'A' + i);
+ else
+ dev_dbg(sitdev->dev, "PLL%c unused by the loaded configuration\n",
+ 'A' + i);
+ }
+}
+
+/*
+ * sit9531x_dev_start - start normal operation
+ *
+ * Fetches initial hardware state, registers all DPLL devices and
+ * their pins, and starts the periodic monitoring thread.
+ */
+int sit9531x_dev_start(struct sit9531x_dev *sitdev)
+{
+ struct sit9531x_dpll *sitdpll;
+ int rc;
+
+ /* Fetch device state */
+ rc = sit9531x_dev_state_fetch(sitdev);
+ if (rc)
+ return rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+ sit9531x_eeprom_state_report(sitdev);
+ sit9531x_manual_sel_report(sitdev);
+ sit9531x_pll_states_report(sitdev);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ list_for_each_entry(sitdpll, &sitdev->dplls, list) {
+ rc = sit9531x_dpll_register(sitdpll);
+ if (rc) {
+ dev_err_probe(sitdev->dev, rc,
+ "Failed to register DPLL%u\n",
+ sitdpll->id);
+ goto err_unregister;
+ }
+ }
+
+ kthread_queue_delayed_work(sitdev->kworker, &sitdev->work, 0);
+
+ return 0;
+
+err_unregister:
+ /*
+ * Unregister what did register. The caller frees the list on this
+ * path, so leaving a DPLL registered would hand the subsystem a
+ * pointer to freed memory.
+ */
+ list_for_each_entry_continue_reverse(sitdpll, &sitdev->dplls, list)
+ sit9531x_dpll_unregister(sitdpll);
+
+ return rc;
+}
+
+/*
+ * sit9531x_dev_stop - stop normal operation
+ *
+ * Cancels the monitoring thread and unregisters all DPLL devices
+ * and their pins.
+ */
+void sit9531x_dev_stop(struct sit9531x_dev *sitdev)
+{
+ struct sit9531x_dpll *sitdpll;
+
+ kthread_cancel_delayed_work_sync(&sitdev->work);
+
+ list_for_each_entry(sitdpll, &sitdev->dplls, list) {
+ if (sitdpll->dpll_dev)
+ sit9531x_dpll_unregister(sitdpll);
+ }
+}
+
+static struct sit9531x_dpll_pin *
+sit9531x_dpll_pin_alloc(struct sit9531x_dpll *sitdpll,
+ enum dpll_pin_direction dir, u8 id)
+{
+ struct sit9531x_dpll_pin *pin;
+
+ pin = kzalloc_obj(*pin, GFP_KERNEL);
+ if (!pin)
+ return ERR_PTR(-ENOMEM);
+
+ pin->dpll = sitdpll;
+ pin->dir = dir;
+ pin->id = id;
+
+ return pin;
+}
+
+/*
+ * sit9531x_dpll_pin_register - register a DPLL pin with the subsystem
+ * @index: absolute pin index for clock_id namespace
+ *
+ * Gets pin properties from firmware, creates or gets a dpll_pin,
+ * and registers it with the parent DPLL device.
+ */
+static int sit9531x_dpll_pin_register(struct sit9531x_dpll_pin *pin,
+ u32 index)
+{
+ struct sit9531x_dpll *sitdpll = pin->dpll;
+ struct sit9531x_pin_props *props;
+ const struct dpll_pin_ops *ops;
+ int rc;
+
+ props = sit9531x_pin_props_get(sitdpll->dev, pin->dir, pin->id);
+ if (IS_ERR(props))
+ return PTR_ERR(props);
+
+ strscpy(pin->label, props->package_label, sizeof(pin->label));
+ pin->fwnode = fwnode_handle_get(props->fwnode);
+
+ pin->dpll_pin = dpll_pin_get(sitdpll->dev->clock_id, index,
+ THIS_MODULE, &props->dpll_props,
+ &pin->tracker);
+ if (IS_ERR(pin->dpll_pin)) {
+ rc = PTR_ERR(pin->dpll_pin);
+ goto err_pin_get;
+ }
+ dpll_pin_fwnode_set(pin->dpll_pin, props->fwnode);
+
+ ops = sit9531x_dpll_pin_ops_get(pin);
+
+ rc = dpll_pin_register(sitdpll->dpll_dev, pin->dpll_pin, ops, pin);
+ if (rc)
+ goto err_register;
+
+ sit9531x_pin_props_put(props);
+
+ return 0;
+
+err_register:
+ dpll_pin_put(pin->dpll_pin, &pin->tracker);
+err_pin_get:
+ /* dpll_pin_get() left an ERR_PTR here. */
+ pin->dpll_pin = NULL;
+ fwnode_handle_put(pin->fwnode);
+ pin->fwnode = NULL;
+ sit9531x_pin_props_put(props);
+
+ return rc;
+}
+
+static void sit9531x_dpll_pin_unregister(struct sit9531x_dpll_pin *pin)
+{
+ struct sit9531x_dpll *sitdpll = pin->dpll;
+ struct dpll_pin *dpll_pin = pin->dpll_pin;
+ const struct dpll_pin_ops *ops;
+
+ ops = sit9531x_dpll_pin_ops_get(pin);
+
+ /*
+ * Clear the pointer before the subsystem is told to drop the pin.
+ * A callback tests this field to tell a pin on its way out from a
+ * live one, and it runs under the subsystem's device lock, which
+ * dpll_pin_unregister() takes as well. Clearing first means such a
+ * callback either sees NULL and leaves the pin alone, or still sees
+ * the pin and finishes before the unregister can proceed. Clearing
+ * afterwards leaves a window where the field still reads as live
+ * and the pin behind it is already gone.
+ */
+ pin->dpll_pin = NULL;
+
+ dpll_pin_unregister(sitdpll->dpll_dev, dpll_pin, ops, pin);
+ dpll_pin_put(dpll_pin, &pin->tracker);
+
+ fwnode_handle_put(pin->fwnode);
+ pin->fwnode = NULL;
+}
+
+static void sit9531x_dpll_pins_unregister(struct sit9531x_dpll *sitdpll)
+{
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ struct sit9531x_dpll_pin *pin, *next;
+ LIST_HEAD(dead);
+
+ /*
+ * Take the pins out of the subsystem first. That call takes the
+ * DPLL device lock, which the driver's own callbacks are already
+ * under when they take multiop_lock, so it must not run with
+ * multiop_lock held or the two orders cross.
+ */
+ list_for_each_entry(pin, &sitdpll->pins, list)
+ sit9531x_dpll_pin_unregister(pin);
+
+ /*
+ * Then detach the list under multiop_lock, which every callback that
+ * walks it holds, so once the splice returns no callback can reach
+ * these nodes. The poll walks the list as well without holding the
+ * lock throughout, but it is not running here: sit9531x_dev_stop()
+ * cancels it before unregistering, and the IRQ that could queue it
+ * again is released before that, its devm action having been added
+ * after the DPLLs were set up. On the registration error path the
+ * poll has not been queued yet. So the nodes are safe to free.
+ */
+ mutex_lock(&sitdev->multiop_lock);
+ list_splice_init(&sitdpll->pins, &dead);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ list_for_each_entry_safe(pin, next, &dead, list) {
+ list_del(&pin->list);
+ kfree(pin);
+ }
+}
+
+/*
+ * sit9531x_dpll_pin_is_registrable - check if a pin should be registered
+ * @dir: pin direction
+ * @index: pin hardware index
+ *
+ * Only the XO pin has a complete pin-op table in this patch, so only
+ * the XO pin is registrable here. Other pin classes are registered
+ * once their state callbacks land in the following patches.
+ *
+ * Return: true if pin should be registered, false otherwise
+ */
+static bool sit9531x_dpll_pin_is_registrable(struct sit9531x_dpll *sitdpll,
+ enum dpll_pin_direction dir,
+ u8 index)
+{
+ /*
+ * Only the XO pin has a complete pin-op table in this patch.
+ * Other pin classes are registered once their state callbacks
+ * land in the following patches.
+ */
+ if (dir != DPLL_PIN_DIRECTION_INPUT)
+ return false;
+
+ return index == SIT9531X_MAX_INPUTS;
+}
+
+/*
+ * sit9531x_dpll_pins_register - register all registrable pins
+ *
+ * Enumerates all possible input and output pins, checks registrability,
+ * and registers each one. Input pins come first, then output pins,
+ * with input pins first, then output pins.
+ */
+static int sit9531x_dpll_pins_register(struct sit9531x_dpll *sitdpll)
+{
+ struct sit9531x_dpll_pin *pin;
+ enum dpll_pin_direction dir;
+ u8 id, index;
+ int rc;
+
+ for (index = 0; index < SIT9531X_NUM_PINS_TOTAL; index++) {
+ if (index < SIT9531X_NUM_INPUT_PINS) {
+ id = index;
+ dir = DPLL_PIN_DIRECTION_INPUT;
+ } else {
+ id = index - SIT9531X_NUM_INPUT_PINS;
+ dir = DPLL_PIN_DIRECTION_OUTPUT;
+ }
+
+ if (!sit9531x_dpll_pin_is_registrable(sitdpll, dir, id))
+ continue;
+
+ pin = sit9531x_dpll_pin_alloc(sitdpll, dir, id);
+ if (IS_ERR(pin)) {
+ rc = PTR_ERR(pin);
+ goto error;
+ }
+
+ rc = sit9531x_dpll_pin_register(pin, index);
+ if (rc) {
+ kfree(pin);
+ goto error;
+ }
+
+ /*
+ * Pins registered before this one are already reachable from
+ * netlink, and the walks over this list run under
+ * multiop_lock, so the insertion does too.
+ */
+ mutex_lock(&sitdpll->dev->multiop_lock);
+ list_add(&pin->list, &sitdpll->pins);
+ mutex_unlock(&sitdpll->dev->multiop_lock);
+ }
+
+ return 0;
+
+error:
+ sit9531x_dpll_pins_unregister(sitdpll);
+ return rc;
+}
+
+static int sit9531x_dpll_device_register(struct sit9531x_dpll *sitdpll)
+{
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ int rc;
+
+ sitdpll->ops = sit9531x_dpll_device_ops;
+
+ sitdpll->dpll_dev = dpll_device_get(sitdev->clock_id, sitdpll->id,
+ THIS_MODULE, &sitdpll->tracker);
+ if (IS_ERR(sitdpll->dpll_dev)) {
+ rc = PTR_ERR(sitdpll->dpll_dev);
+ sitdpll->dpll_dev = NULL;
+ return rc;
+ }
+
+ rc = dpll_device_register(sitdpll->dpll_dev,
+ sit9531x_prop_dpll_type_get(sitdev,
+ sitdpll->id),
+ &sitdpll->ops, sitdpll);
+ if (rc) {
+ dpll_device_put(sitdpll->dpll_dev, &sitdpll->tracker);
+ sitdpll->dpll_dev = NULL;
+ }
+
+ return rc;
+}
+
+static void sit9531x_dpll_device_unregister(struct sit9531x_dpll *sitdpll)
+{
+ dpll_device_unregister(sitdpll->dpll_dev, &sitdpll->ops, sitdpll);
+ dpll_device_put(sitdpll->dpll_dev, &sitdpll->tracker);
+ sitdpll->dpll_dev = NULL;
+}
+
+/*
+ * sit9531x_dpll_alloc - allocate a DPLL device structure
+ * @sitdev: parent device
+ * @ch: PLL channel number (0-3)
+ *
+ * Return: pointer to allocated DPLL on success, error pointer on error
+ */
+struct sit9531x_dpll *sit9531x_dpll_alloc(struct sit9531x_dev *sitdev, u8 ch)
+{
+ struct sit9531x_dpll *sitdpll;
+
+ sitdpll = kzalloc_obj(*sitdpll, GFP_KERNEL);
+ if (!sitdpll)
+ return ERR_PTR(-ENOMEM);
+
+ sitdpll->dev = sitdev;
+ sitdpll->id = ch;
+ sitdpll->lock_status = DPLL_LOCK_STATUS_UNLOCKED;
+ sitdpll->lock_status_error = DPLL_LOCK_STATUS_ERROR_NONE;
+ INIT_LIST_HEAD(&sitdpll->pins);
+
+ return sitdpll;
+}
+
+/*
+ * sit9531x_dpll_free - deallocate a DPLL device structure
+ * @sitdpll: DPLL to free
+ */
+void sit9531x_dpll_free(struct sit9531x_dpll *sitdpll)
+{
+ kfree(sitdpll);
+}
+
+/*
+ * sit9531x_dpll_register - register DPLL device and all its pins
+ *
+ * Registers the DPLL device with the subsystem and then registers
+ * all input and output pins that are connected to this PLL.
+ */
+int sit9531x_dpll_register(struct sit9531x_dpll *sitdpll)
+{
+ int rc;
+
+ rc = sit9531x_dpll_device_register(sitdpll);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_dpll_pins_register(sitdpll);
+ if (rc) {
+ sit9531x_dpll_device_unregister(sitdpll);
+ return rc;
+ }
+
+ return 0;
+}
+
+/* sit9531x_dpll_unregister - unregister DPLL device and its pins */
+void sit9531x_dpll_unregister(struct sit9531x_dpll *sitdpll)
+{
+ sit9531x_dpll_pins_unregister(sitdpll);
+ sit9531x_dpll_device_unregister(sitdpll);
+}
+
+static void sit9531x_dpll_list_free(struct sit9531x_dev *sitdev)
+{
+ struct sit9531x_dpll *sitdpll, *next;
+
+ list_for_each_entry_safe(sitdpll, next, &sitdev->dplls, list) {
+ list_del(&sitdpll->list);
+ sit9531x_dpll_free(sitdpll);
+ }
+}
+
+/* Runs only once the device is fully started, see the caller. */
+static void sit9531x_dev_dpll_fini(void *ptr)
+{
+ struct sit9531x_dev *sitdev = ptr;
+
+ sit9531x_dev_stop(sitdev);
+ kthread_destroy_worker(sitdev->kworker);
+ sit9531x_dpll_list_free(sitdev);
+}
+
+static int sit9531x_devm_dpll_init(struct sit9531x_dev *sitdev)
+{
+ struct kthread_worker *kworker;
+ struct sit9531x_dpll *sitdpll;
+ unsigned int i;
+ int rc;
+
+ INIT_LIST_HEAD(&sitdev->dplls);
+ kthread_init_delayed_work(&sitdev->work, sit9531x_dev_periodic_work);
+
+ for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+ sitdpll = sit9531x_dpll_alloc(sitdev, i);
+ if (IS_ERR(sitdpll)) {
+ rc = dev_err_probe(sitdev->dev, PTR_ERR(sitdpll),
+ "Failed to alloc DPLL%u\n", i);
+ goto err_free_dplls;
+ }
+
+ list_add_tail(&sitdpll->list, &sitdev->dplls);
+ }
+
+ kworker = kthread_run_worker(0, "sit9531x-%s", dev_name(sitdev->dev));
+ if (IS_ERR(kworker)) {
+ rc = PTR_ERR(kworker);
+ goto err_free_dplls;
+ }
+ sitdev->kworker = kworker;
+
+ rc = sit9531x_dev_start(sitdev);
+ if (rc) {
+ rc = dev_err_probe(sitdev->dev, rc, "Failed to start device\n");
+ goto err_destroy_worker;
+ }
+
+ /*
+ * Only now is every field the cleanup touches valid, so this is the
+ * first point at which the action may be registered. On failure it
+ * runs the action itself, which is correct here and only here.
+ */
+ return devm_add_action_or_reset(sitdev->dev, sit9531x_dev_dpll_fini,
+ sitdev);
+
+err_destroy_worker:
+ kthread_destroy_worker(sitdev->kworker);
+err_free_dplls:
+ sit9531x_dpll_list_free(sitdev);
+
+ return rc;
+}
+
+/*
+ * sit9531x_read_variant_id - read chip variant ID byte from hardware
+ * @id: output variant ID byte
+ *
+ * Reads the single-byte variant identification register from Page 0
+ * reg 0x02 (95317 = 0x17, 95316 = 0x31). Reg 0x03 holds a separate
+ * revision byte and is intentionally not consumed here.
+ */
+static int sit9531x_read_variant_id(struct sit9531x_dev *sitdev, u8 *id)
+{
+ return sit9531x_read_u8(sitdev, SIT9531X_REG_VARIANT_ID, id);
+}
+
+static const struct sit9531x_chip_info *sit9531x_match_variant(u8 id)
+{
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(sit9531x_chip_ids); i++) {
+ if (sit9531x_chip_ids[i].id == id)
+ return &sit9531x_chip_ids[i];
+ }
+
+ return NULL;
+}
+
+/*
+ * sit9531x_derive_clock_id - build EUI-64 clock identifier
+ *
+ * Generates a deterministic 64-bit identifier from the SiTime OUI,
+ * the I2C bus number and the I2C address. The bus number
+ * disambiguates two same-variant parts at the same address on
+ * different adapters (or behind a mux), which the DPLL core would
+ * otherwise fold onto one set of objects; DT bus aliases keep the
+ * numbering, and with it the clock_id, stable across reboots.
+ *
+ * Return: 64-bit clock identifier
+ */
+static u64 sit9531x_derive_clock_id(struct sit9531x_dev *sitdev)
+{
+ u64 clkid;
+ int adap;
+
+ adap = i2c_adapter_id(sitdev->client->adapter);
+
+ /*
+ * The OUI with its EUI-64 filler takes the top 40 bits. Bus numbers
+ * are not bounded by the number of adapters -- a DT alias or a mux
+ * child can number a bus 256 or above on a board with a handful of
+ * them -- so the bus gets 16 bits. The variant does not need a
+ * field: one address on one bus holds one part.
+ */
+ clkid = SIT9531X_OUI << 24;
+ clkid |= (u64)(adap & 0xffff) << 8;
+ clkid |= (u64)sitdev->client->addr;
+
+ return clkid;
+}
+
+int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
+{
+ struct clk *xtal_clk;
+ u8 variant_id;
+ int rc;
+
+ /*
+ * Fvco = Fref * (DIVN + frac/2^32) with Fref = xtal_freq << doubler,
+ * so every freq_set and phase_adjust path divides by a rate derived
+ * from the XO feeding XIN/XO_CLK. The rate normally comes from a
+ * "clocks" phandle (clock-names = "xtal"). On platforms where the
+ * firmware does not expose the XO through the clock framework, fall
+ * back to a "clock-frequency" device property.
+ */
+ xtal_clk = devm_clk_get_optional_enabled(sitdev->dev, "xtal");
+ if (IS_ERR(xtal_clk))
+ return dev_err_probe(sitdev->dev, PTR_ERR(xtal_clk),
+ "Failed to get xtal clock\n");
+ sitdev->xtal_freq = clk_get_rate(xtal_clk);
+ if (!sitdev->xtal_freq) {
+ u32 freq;
+
+ if (!device_property_read_u32(sitdev->dev, "clock-frequency",
+ &freq))
+ sitdev->xtal_freq = freq;
+ }
+ if (!sitdev->xtal_freq)
+ return dev_err_probe(sitdev->dev, -EINVAL,
+ "no xtal rate: provide clocks=<&xo> + clock-names=\"xtal\", or a clock-frequency property\n");
+
+ /*
+ * Held deasserted, never pulsed: the chip configuration comes from
+ * efuse or an NVM blob applied before probe, and a reset would
+ * discard it. Must precede the first I2C access, as a board that
+ * powers up asserted keeps the chip unreachable until released.
+ */
+ sitdev->reset_gpio = devm_gpiod_get_optional(sitdev->dev, "reset",
+ GPIOD_OUT_LOW);
+ if (IS_ERR(sitdev->reset_gpio))
+ return dev_err_probe(sitdev->dev, PTR_ERR(sitdev->reset_gpio),
+ "Failed to request reset gpio\n");
+ if (sitdev->reset_gpio)
+ fsleep(10000); /* internal boot after release */
+
+ rc = sit9531x_read_variant_id(sitdev, &variant_id);
+ if (rc)
+ return rc;
+
+ sitdev->info = sit9531x_match_variant(variant_id);
+ if (!sitdev->info)
+ return dev_err_probe(sitdev->dev, -ENODEV,
+ "Unknown variant ID: 0x%02x\n",
+ variant_id);
+
+ sitdev->clock_id = sit9531x_derive_clock_id(sitdev);
+ sitdev->intsync_src = -1;
+
+ rc = devm_mutex_init(sitdev->dev, &sitdev->multiop_lock);
+ if (rc)
+ return dev_err_probe(sitdev->dev, rc,
+ "Failed to initialize mutex\n");
+
+ dev_info(sitdev->dev, "%s detected, %u inputs, %u outputs\n",
+ sitdev->info->name, sitdev->info->num_inputs,
+ sitdev->info->num_outputs);
+
+ /*
+ * Before the IRQ: the handler reaches sitdev->kworker through
+ * kthread_mod_delayed_work(), so the worker has to exist before an
+ * INTRB assertion can land.
+ */
+ rc = sit9531x_devm_dpll_init(sitdev);
+ if (rc)
+ return rc;
+
+ /* Absent "interrupts" leaves client->irq 0 and the poll in charge. */
+ sitdev->irq = sitdev->client ? sitdev->client->irq : 0;
+ if (sitdev->irq > 0) {
+ rc = devm_request_threaded_irq(sitdev->dev, sitdev->irq,
+ NULL, sit9531x_irq_thread_fn,
+ IRQF_ONESHOT,
+ dev_name(sitdev->dev), sitdev);
+ if (rc)
+ return dev_err_probe(sitdev->dev, rc,
+ "Failed to request IRQ %d\n",
+ sitdev->irq);
+ }
+
+ return 0;
+}
+
+static int sit9531x_i2c_probe(struct i2c_client *client)
+{
+ struct sit9531x_dev *sitdev;
+ struct regmap *regmap;
+
+ regmap = devm_regmap_init_i2c(client, &sit9531x_regmap_config);
+ if (IS_ERR(regmap))
+ return dev_err_probe(&client->dev, PTR_ERR(regmap),
+ "Failed to initialize regmap\n");
+
+ sitdev = devm_kzalloc(&client->dev, sizeof(*sitdev), GFP_KERNEL);
+ if (!sitdev)
+ return -ENOMEM;
+
+ sitdev->dev = &client->dev;
+ sitdev->client = client;
+ sitdev->regmap = regmap;
+ i2c_set_clientdata(client, sitdev);
+
+ return sit9531x_dev_probe(sitdev);
}
static const struct of_device_id sit9531x_of_match[] = {
@@ -305,10 +1889,45 @@ static const struct of_device_id sit9531x_of_match[] = {
};
MODULE_DEVICE_TABLE(of, sit9531x_of_match);
+/*
+ * The poll worker is not freezable and would keep issuing paged I2C
+ * transfers into a suspended adapter, where i2c_transfer() fails and a
+ * tick landing mid-suspend could tear a paged sequence between the
+ * page-selector write and the register access. Park the worker (and
+ * the IRQ that kicks it) across suspend and take a fresh sample on
+ * resume.
+ */
+static int sit9531x_suspend(struct device *dev)
+{
+ struct sit9531x_dev *sitdev = dev_get_drvdata(dev);
+
+ if (sitdev->irq > 0)
+ disable_irq(sitdev->irq);
+ kthread_cancel_delayed_work_sync(&sitdev->work);
+
+ return 0;
+}
+
+static int sit9531x_resume(struct device *dev)
+{
+ struct sit9531x_dev *sitdev = dev_get_drvdata(dev);
+
+ sit9531x_page_cache_drop(sitdev);
+ if (sitdev->irq > 0)
+ enable_irq(sitdev->irq);
+ kthread_queue_delayed_work(sitdev->kworker, &sitdev->work, 0);
+
+ return 0;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(sit9531x_pm_ops,
+ sit9531x_suspend, sit9531x_resume);
+
static struct i2c_driver sit9531x_i2c_driver = {
.driver = {
.name = "sit9531x",
.of_match_table = sit9531x_of_match,
+ .pm = pm_sleep_ptr(&sit9531x_pm_ops),
},
.probe = sit9531x_i2c_probe,
};
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 4b4f72c8622b..28fc99f50063 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -15,6 +15,8 @@
#include <linux/gpio/consumer.h>
#include <linux/i2c.h>
+#include <linux/kthread.h>
+#include <linux/list.h>
#include <linux/mutex.h>
#include <linux/regmap.h>
#include <linux/types.h>
@@ -34,6 +36,15 @@
*/
#define SIT9531X_INTSYNC_PIN_ID (SIT9531X_MAX_INPUTS + 1)
#define SIT9531X_INTSYNC_OUT_PIN_ID SIT9531X_MAX_OUTPUTS
+#define SIT9531X_STATUS_POLL_MS 500
+
+/* selected_ref value when the active source is not a registered input */
+#define SIT9531X_REF_INVALID 0xFF
+
+/* SiTime IEEE OUI for EUI-64 generation */
+#define SIT9531X_OUI 0x0090C2FFFEULL
+
+struct sit9531x_dpll;
/*
* struct sit9531x_chip_info - chip variant identification
@@ -64,33 +75,105 @@ enum sit9531x_signal_mode {
/*
* struct sit9531x_ref - input reference state
* @freq: configured frequency in Hz
+ * @enabled: the lane's receiver is on
+ * @pll_mask: bitmask of PLLs this input feeds (bit 0 = PLLA)
* @sig_mode: signal mode of the pair this lane belongs to
* (detected from CLKINx_INPUT_MODE at probe)
*/
struct sit9531x_ref {
u64 freq;
+ bool enabled;
+ u8 pll_mask;
enum sit9531x_signal_mode sig_mode;
};
/*
* struct sit9531x_out - output state
- * @freq: configured frequency in Hz
+ * @freq: rate the output is running at, in Hz
+ * @enabled: output is driving, i.e. not forced into Hi-Z
+ * @cmos: output is wired single-ended; the Hi-Z pairs that
+ * speak for it are the two CMOS pad ones, not the
+ * differential
+ * @routed: output is mapped to @pll_idx by the initial
+ * configuration; an unrouted output has no DPLL pin
+ * @pll_idx: PLL driving this output (0-3)
*/
struct sit9531x_out {
u64 freq;
+ bool enabled;
+ bool cmos;
+ bool routed;
+ u8 pll_idx;
+};
+
+/*
+ * struct sit9531x_chan - per-PLL channel state
+ * @active: PLL has reached its active state; a PLL the loaded
+ * configuration leaves unused never does, and its
+ * loss-of-lock bit stays clear because nothing drives it
+ * @locked: PLL is active and its outer loop reports lock
+ * @mode: 0 = sync (outer loop enabled), 1 = free-run
+ * @selected_ref: logical input index of the currently selected
+ * reference (the INTSYNC net maps to
+ * SIT9531X_INTSYNC_PIN_ID), or SIT9531X_REF_INVALID
+ * when the hardware source encoding is reserved
+ * @inner_lol: PLL inner loop loss-of-lock detected
+ * @ho_freeze: holdover freeze active
+ * @ho_valid: holdover memory acquired, i.e. the holdover window
+ * holds a valid estimate to fall back on
+ * @prio_mask: bit per hardware source code present in this PLL's
+ * priority table, i.e. the sources it may select. Read
+ * back from the table by the periodic worker and
+ * refreshed by every table write, so it tracks the
+ * hardware rather than the driver's intent
+ * @cfg_prio: priority configured for each canonical source,
+ * whether or not it is in the table; the table is
+ * built from it, so taking a source out and putting it
+ * back does not change anyone's priority
+ * @cfg_known: bit per canonical source that has a @cfg_prio
+ * @seen_srcs: the table as the driver last wrote or seeded
+ * @cfg_prio from; a read-back that differs means
+ * something else rewrote it, and @cfg_prio is
+ * re-seeded from the hardware
+ * @seen_valid: @seen_srcs holds a table
+ */
+struct sit9531x_chan {
+ bool active;
+ bool locked;
+ u8 mode;
+ u8 selected_ref;
+ bool inner_lol;
+ bool ho_freeze;
+ bool ho_valid;
+ u16 prio_mask;
+ u8 cfg_prio[SIT9531X_PRIO_NUM_SRC];
+ u16 cfg_known;
+ u8 seen_srcs[SIT9531X_PRIO_MAX_SLOTS];
+ bool seen_valid;
};
/*
* struct sit9531x_dev - SiT9531x device instance
- * @dev: parent device
- * @client: I2C client
- * @regmap: paged register map
* @info: detected chip variant info
- * @multiop_lock: serializes multi-register sequences
+ * @dev: parent device
+ * @client: I2C client backing @regmap
+ * @regmap: paged register map of the device
+ * @dplls: DPLL devices registered for this chip
+ * @multiop_lock: mutex for multi-register atomic operations
* @ref: array of input reference states
* @out: array of output states
+ * @chan: array of per-PLL channel states
* @xtal_freq: crystal oscillator frequency in Hz
+ * @kworker: kthread worker for periodic polling
+ * @work: delayed work for periodic state checks
+ * @clock_id: IEEE 1588 EUI-64 clock identifier
* @reset_gpio: optional reset line (DT "reset-gpios"), NULL if absent
+ * @irq: optional INTRB IRQ number (from DT "interrupts" via the
+ * I2C client), 0 if no IRQ is wired
+ * @intsync_src: PLL index currently sourcing inter-PLL
+ * synchronization (INTSYNC), or -1 when disabled
+ * @irq_ack_fails: consecutive failures to acknowledge the
+ * notification latches from the interrupt handler
*/
struct sit9531x_dev {
struct device *dev;
@@ -101,13 +184,78 @@ struct sit9531x_dev {
struct mutex multiop_lock;
/* Hardware state */
+ u8 irq_ack_fails;
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;
+
+ /* 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 ---- */
+
+/* ---- Input priority ---- */
+
+/* ---- Output enable/disable (Hi-Z control) ---- */
+
+/* ---- Output frequency ---- */
+
+/* ---- Output phase adjust (PRG_RST_DELAY register-based) ---- */
+
+/* ---- Notification clear ---- */
+int sit9531x_clear_notifications(struct sit9531x_dev *sitdev);
+
+/* ---- INTSYNC (inter-PLL synchronization) ---- */
+
+/* ---- Phase offset (TDC readback) ---- */
+
+/* ---- 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, ...).
@@ -133,27 +281,95 @@ static inline bool sit9531x_input_is_n(u8 index)
}
/*
- * sit9531x_pll_page - get register page for PLL index
- * @pll_idx: PLL index (0 = PLLA, 3 = PLLD)
+ * sit9531x_input_hw_src - translate logical input index to source encoding
+ * @index: logical input pin index
+ *
+ * The priority table and CLK_ACTIVESEL registers use a non-contiguous
+ * source encoding: 0-3 = CLK0P..CLK3P, 5 = OCXO, 6 = INTSYNC,
+ * 7-10 = CLK0N..CLK3N.
*/
-static inline u8 sit9531x_pll_page(u8 pll_idx)
+static inline u8 sit9531x_input_hw_src(u8 index)
{
- return SIT9531X_PAGE_PLLA + pll_idx;
+ if (index == SIT9531X_MAX_INPUTS)
+ return SIT9531X_PRIO_SRC_OCXO;
+ if (index == SIT9531X_INTSYNC_PIN_ID)
+ return SIT9531X_PRIO_SRC_INTSYNC;
+ if (sit9531x_input_is_n(index))
+ return SIT9531X_PRIO_SRC_N_BASE + sit9531x_input_pair(index);
+ return sit9531x_input_pair(index);
}
-extern const struct regmap_config sit9531x_regmap_config;
+/*
+ * sit9531x_hw_src_input - translate source encoding to logical input index
+ * @src: 4-bit hardware source encoding
+ *
+ * Return: logical input index (INTSYNC maps to SIT9531X_INTSYNC_PIN_ID),
+ * or SIT9531X_REF_INVALID if @src is a reserved value
+ */
+static inline u8 sit9531x_hw_src_input(u8 src)
+{
+ if (src < SIT9531X_NUM_INPUT_PAIRS)
+ return src * 2;
+ if (src == SIT9531X_PRIO_SRC_OCXO)
+ return SIT9531X_MAX_INPUTS;
+ if (src == SIT9531X_PRIO_SRC_INTSYNC)
+ return SIT9531X_INTSYNC_PIN_ID;
+ if (src >= SIT9531X_PRIO_SRC_N_BASE &&
+ src < SIT9531X_PRIO_SRC_N_BASE + SIT9531X_NUM_INPUT_PAIRS)
+ return (src - SIT9531X_PRIO_SRC_N_BASE) * 2 + 1;
+ return SIT9531X_REF_INVALID;
+}
-/* ---- Core lifecycle ---- */
-int sit9531x_dev_probe(struct sit9531x_dev *sitdev);
+/*
+ * sit9531x_prio_src_usable - does a priority slot name a reference?
+ * @src: 4-bit hardware source encoding held in a table slot
+ *
+ * Codes 4 and 11 address a fifth input pair this part does not have, and
+ * a value outside the encoding is not a source either, so a slot holding
+ * one of them names nothing the device can select. Canonicalizing an N
+ * lane to its pair first would not change the answer -- both lanes are
+ * references -- so this needs no device state.
+ *
+ * Return: true when the code resolves to a reference the device can use
+ */
+static inline bool sit9531x_prio_src_usable(u8 src)
+{
+ return sit9531x_hw_src_input(src) != SIT9531X_REF_INVALID;
+}
-/* ---- Register access ---- */
-int sit9531x_read_u8(struct sit9531x_dev *sitdev, unsigned int reg, u8 *val);
-int sit9531x_write_u8(struct sit9531x_dev *sitdev, unsigned int reg, u8 val);
-int sit9531x_read_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx, u8 offset,
- u8 *val);
-int sit9531x_write_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx, u8 offset,
- u8 val);
-int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx, u8 offset,
- u8 mask, u8 val);
+/*
+ * sit9531x_ref_state_get - get reference state by index
+ * @index: logical input index
+ *
+ * Return: pointer to the cached input reference state
+ */
+static inline const struct sit9531x_ref *
+sit9531x_ref_state_get(const struct sit9531x_dev *sitdev, u8 index)
+{
+ return &sitdev->ref[index];
+}
+
+/*
+ * sit9531x_out_state_get - get output state by index
+ * @index: logical output index
+ *
+ * Return: pointer to the cached output state
+ */
+static inline const struct sit9531x_out *
+sit9531x_out_state_get(const struct sit9531x_dev *sitdev, u8 index)
+{
+ return &sitdev->out[index];
+}
+
+/*
+ * sit9531x_chan_state_get - get channel state by PLL index
+ *
+ * Return: pointer to the cached per-PLL channel state
+ */
+static inline const struct sit9531x_chan *
+sit9531x_chan_state_get(const struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+ return &sitdev->chan[pll_idx];
+}
#endif /* _SIT9531X_CORE_H */
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
new file mode 100644
index 000000000000..924386aec4d8
--- /dev/null
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -0,0 +1,389 @@
+// 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"
+
+static bool sit9531x_dpll_is_input_pin(const struct sit9531x_dpll_pin *pin)
+{
+ return pin->dir == DPLL_PIN_DIRECTION_INPUT;
+}
+
+static bool
+sit9531x_dpll_is_xo_pin(const struct sit9531x_dpll_pin *pin)
+{
+ return sit9531x_dpll_is_input_pin(pin) &&
+ pin->id == SIT9531X_MAX_INPUTS;
+}
+
+/*
+ * The cached state this reports comes from the outer loss-of-lock byte
+ * (page 0, reg 0x06), the PLL mode bit (PLL page, reg 0x31), inner LOL
+ * (reg 0x92), the holdover freeze byte (reg 0x0A) and the per-PLL
+ * holdover-valid bit (PLL page, reg 0x06).
+ */
+static int
+sit9531x_dpll_lock_status_get(const struct dpll_device *dpll, void *dpll_priv,
+ enum dpll_lock_status *status,
+ enum dpll_lock_status_error *status_error,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ const struct sit9531x_chan *chan;
+
+ if (status_error)
+ *status_error = DPLL_LOCK_STATUS_ERROR_NONE;
+
+ chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ if (!chan->active) {
+ /*
+ * A PLL the loaded configuration leaves unused never reaches
+ * its active state. Nothing drives its loss-of-lock bit, so
+ * without this it would report a lock it does not have.
+ */
+ *status = DPLL_LOCK_STATUS_UNLOCKED;
+ } else if (chan->inner_lol) {
+ /*
+ * The core publishes the error detail only for the unlocked
+ * and holdover states, so an inner loss of lock reported
+ * under a locked status would never reach userspace. An
+ * inner loop that is not locked is not a locked PLL.
+ */
+ *status = chan->ho_freeze ? DPLL_LOCK_STATUS_HOLDOVER :
+ DPLL_LOCK_STATUS_UNLOCKED;
+ } else if (chan->mode) {
+ /*
+ * Free-run: the outer loop is disabled, so the PLL tracks no
+ * reference at all and its loss-of-lock bit means nothing.
+ * That is what UNLOCKED describes -- "not yet locked to any
+ * valid input (or was forced by user)".
+ */
+ *status = DPLL_LOCK_STATUS_UNLOCKED;
+ } else if (chan->locked) {
+ /*
+ * HO_ACQ is locked *and* holdover memory acquired, so it needs
+ * the holdover-valid bit rather than following from the lock.
+ */
+ if (chan->ho_valid)
+ *status = DPLL_LOCK_STATUS_LOCKED_HO_ACQ;
+ else
+ *status = DPLL_LOCK_STATUS_LOCKED;
+ } else if (chan->ho_freeze) {
+ *status = DPLL_LOCK_STATUS_HOLDOVER;
+ } else {
+ *status = DPLL_LOCK_STATUS_UNLOCKED;
+ }
+
+ /* Report inner LOL as an error condition */
+ if (status_error && chan->inner_lol)
+ *status_error = DPLL_LOCK_STATUS_ERROR_UNDEFINED;
+
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return 0;
+}
+
+/*
+ * Mode
+ * ====
+ * enum dpll_mode differentiates how a DPLL selects an input: AUTOMATIC
+ * has the device pick the highest-priority one, MANUAL has userspace
+ * request one. The device only implements the former through this
+ * driver, so AUTOMATIC is the only mode advertised.
+ *
+ * With a single mode there is nothing to switch, so no .mode_set: the core
+ * answers a mode request with -EOPNOTSUPP. Free-run -- the outer loop
+ * disabled through PLL page reg 0x31[5] -- is not a mode in those terms,
+ * because no input is selected either way. It is reported through lock
+ * status instead, and entered and left through the chip-specific tool
+ * rather than over netlink.
+ *
+ * The device could implement real MANUAL: PLL_CONFIG1F_PLL (PLL page reg
+ * 0x1F) bit 6 switches a PLL from priority-based to manual active select,
+ * and with MISCINNER_PLL (reg 0x18) bit 5 the PLL follows a manual input
+ * select -- the input-select pins, or with GPIO_INPUT_FUNC_CTRL5..8
+ * (page 0, regs 0xE8-0xEB) bit 4 the register's own low nibble -- which
+ * pins one reference while the loop keeps running. Wiring that up would
+ * let .state_on_dpll_set() accept CONNECTED; it needs bench validation
+ * first, and regs 0x18 and 0x1F carry GUI-generated configuration in
+ * their other bits, so it is left out until then. Advertising MANUAL and
+ * then refusing the one request MANUAL exists for is the worse of the two
+ * incomplete answers, and after merge it would be ABI. A profile that
+ * sets manual select anyway is reported at probe, since the mode this
+ * driver reports would then be wrong.
+ */
+static int
+sit9531x_dpll_mode_get(const struct dpll_device *dpll, void *dpll_priv,
+ enum dpll_mode *mode, struct netlink_ext_ack *extack)
+{
+ *mode = DPLL_MODE_AUTOMATIC;
+
+ return 0;
+}
+
+static int
+sit9531x_dpll_supported_modes_get(const struct dpll_device *dpll,
+ void *dpll_priv, unsigned long *modes,
+ struct netlink_ext_ack *extack)
+{
+ __set_bit(DPLL_MODE_AUTOMATIC, modes);
+
+ return 0;
+}
+
+const struct dpll_device_ops sit9531x_dpll_device_ops = {
+ .lock_status_get = sit9531x_dpll_lock_status_get,
+ .mode_get = sit9531x_dpll_mode_get,
+ .supported_modes_get = sit9531x_dpll_supported_modes_get,
+ /* temp_get not available -- SiT9531x has no on-die temp sensor */
+};
+
+/*
+ * Pin-state contract
+ * ==================
+ * The five pin ops tables below fall into three roles, and only the first
+ * has a selection state machine. Each state_on_dpll callback implements
+ * the rules for its role and nothing else, so the tables cannot drift
+ * apart the way five independent encodings of this did.
+ *
+ * SELECTION role -- physical input pins, INTSYNC destination pin.
+ * The state is what userspace asked for; what the device is doing with
+ * the pin is the operational state. Predicates, all evaluated under
+ * multiop_lock:
+ * M source is present in THIS PLL's hardware priority table
+ * S chan->selected_ref == this pin's id (the active selection)
+ * L chan->locked && !chan->mode && !chan->ho_freeze
+ * (tracking a reference: outer loop running, locked, not frozen)
+ * N the input lane's clock monitor reports loss of signal
+ * Q the lane's monitor reports a frequency drift, with signal
+ * state get:
+ * SELECTABLE M
+ * DISCONNECTED !M
+ * operstate get:
+ * ACTIVE S && L && !N
+ * NO_SIGNAL N
+ * QUAL_FAILED !N && Q && !(S && L)
+ * STANDBY otherwise
+ * set:
+ * DISCONNECTED remove from this PLL's table; a physical input also
+ * releases this DPLL's claim and powers the shared
+ * receiver down on the last release
+ * SELECTABLE add to this PLL's table; a physical input powers the
+ * receiver up and takes the claim, in that order
+ * CONNECTED -EOPNOTSUPP -- the device selects by priority and has
+ * no mode that pins one reference (see "Mode" above)
+ * other -EINVAL
+ *
+ * The ACTIVE test needs L as well as S because the selection is what the
+ * driver last wrote or the device last chose, not proof the loop uses
+ * it: a free-running, frozen or unlocked PLL follows nothing. It needs
+ * !N because a PLL whose selection names a lane without signal has
+ * fallen back to another listed source on its own, and no register
+ * says which -- no pin is reported active then. The INTSYNC destination
+ * has no monitor, so N and Q never hold for it.
+ *
+ * M is read from the hardware priority table, not from ref->pll_mask,
+ * which is only the shared-receiver refcount and says nothing about one
+ * DPLL's eligibility. Priority is kept by the driver per source and
+ * PLL, independent of M, so a pin reports the same priority whether it
+ * is connected or not.
+ *
+ * DRIVE role -- output pins, INTSYNC source pin.
+ * Is this pin or net being driven? Nothing is selected here, so:
+ * CONNECTED pin or net is driven
+ * DISCONNECTED pin is muted (Hi-Z), or this PLL does not drive it
+ * SELECTABLE -EINVAL on set, never reported by get
+ *
+ * FIXED role -- XO pin. Always CONNECTED; it cannot be routed.
+ */
+
+static int
+sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_direction *direction,
+ struct netlink_ext_ack *extack)
+{
+ *direction = DPLL_PIN_DIRECTION_INPUT;
+ return 0;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
+ .direction_get = sit9531x_dpll_input_pin_direction_get,
+};
+
+/*
+ * INTSYNC pin ops
+ *
+ * INTSYNC is the chip's inter-PLL sync net: one PLL drives it and other
+ * PLLs may lock to it instead of to an external reference. The two
+ * roles are exposed as two separate pins so neither overloads the other:
+ *
+ * - a source (output) pin registered on every DPLL. Connecting it on a
+ * DPLL makes that DPLL drive INTSYNC; only one DPLL may drive it at a
+ * time. It has no priority ops -- driving the net is not a reference
+ * selection.
+ * - a destination (input) pin registered on every DPLL. Connecting it
+ * on a DPLL makes that DPLL eligible to lock to INTSYNC as a
+ * reference, so it carries the priority ops.
+ */
+
+/* ---- INTSYNC source (output) pin ---- */
+
+/* The INTSYNC source pin is an output; its direction_get is defined below. */
+static int
+sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_direction *direction,
+ struct netlink_ext_ack *extack);
+
+/* ---- INTSYNC destination (input) pin ---- */
+
+/*
+ * XO (crystal oscillator) pin ops
+ *
+ * The XO is the chip's internal reference oscillator that feeds every
+ * PLL. It is exposed so userspace can see the on-chip reference, but it
+ * cannot be routed or disconnected, so it is reported permanently
+ * connected and offers no state_on_dpll_set / prio ops.
+ */
+
+static int
+sit9531x_dpll_xo_pin_state_on_dpll_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state *state,
+ struct netlink_ext_ack *extack)
+{
+ *state = DPLL_PIN_STATE_CONNECTED;
+ return 0;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_xo_pin_ops = {
+ .direction_get = sit9531x_dpll_input_pin_direction_get,
+ .state_on_dpll_get = sit9531x_dpll_xo_pin_state_on_dpll_get,
+};
+
+static int
+sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_direction *direction,
+ struct netlink_ext_ack *extack)
+{
+ *direction = DPLL_PIN_DIRECTION_OUTPUT;
+ return 0;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_output_pin_ops = {
+ .direction_get = sit9531x_dpll_output_pin_direction_get,
+};
+
+const struct dpll_pin_ops *
+sit9531x_dpll_pin_ops_get(const struct sit9531x_dpll_pin *pin)
+{
+ if (!sit9531x_dpll_is_input_pin(pin))
+ return &sit9531x_dpll_output_pin_ops;
+ if (sit9531x_dpll_is_xo_pin(pin))
+ return &sit9531x_dpll_xo_pin_ops;
+ return &sit9531x_dpll_input_pin_ops;
+}
+
+/*
+ * sit9531x_dpll_changes_check - check for state changes and notify
+ *
+ * Called from sit9531x_dev_periodic_work(). Compares current hardware
+ * state against cached values and sends netlink notifications on changes.
+ */
+void sit9531x_dpll_changes_check(struct sit9531x_dpll *sitdpll)
+{
+ enum dpll_lock_status_error status_error = DPLL_LOCK_STATUS_ERROR_NONE;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ enum dpll_lock_status lock_status;
+ struct sit9531x_dpll_pin *pin;
+ int rc;
+
+ rc = sit9531x_dpll_lock_status_get(sitdpll->dpll_dev, sitdpll,
+ &lock_status, &status_error, NULL);
+ if (rc) {
+ dev_err(sitdev->dev, "Failed to get DPLL%u lock status: %d\n",
+ sitdpll->id, rc);
+ return;
+ }
+
+ /*
+ * The core publishes the error detail alongside the status, so a
+ * change in either is a change subscribers have to be told about:
+ * an inner loss of lock appearing or clearing while the status
+ * stays UNLOCKED would otherwise be visible only to a later GET.
+ */
+ if (sitdpll->lock_status != lock_status ||
+ sitdpll->lock_status_error != status_error) {
+ sitdpll->lock_status = lock_status;
+ sitdpll->lock_status_error = status_error;
+ dpll_device_change_ntf(sitdpll->dpll_dev);
+ }
+
+ list_for_each_entry(pin, &sitdpll->pins, list) {
+ const struct dpll_pin_ops *ops;
+ enum dpll_pin_state state;
+ bool changed;
+
+ /*
+ * 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 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;
+
+ /*
+ * The first pass only takes the baseline: the pin was
+ * registered with this state, so nothing has changed yet.
+ */
+ changed = pin->seen && state != pin->pin_state;
+ pin->pin_state = state;
+ pin->seen = true;
+ if (changed) {
+ dev_dbg(sitdev->dev, "%s state changed\n", pin->label);
+ 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..1d320e183eb6
--- /dev/null
+++ b/drivers/dpll/sit9531x/dpll.h
@@ -0,0 +1,67 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * SiTime SiT9531x DPLL subsystem interface
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * DPLL device and pin structures, and function declarations for
+ * the DPLL registration and callback layer.
+ */
+
+#ifndef _SIT9531X_DPLL_H
+#define _SIT9531X_DPLL_H
+
+#include <linux/dpll.h>
+#include <linux/list.h>
+#include <linux/types.h>
+
+struct sit9531x_dev;
+
+/* Per-pin DPLL state. */
+struct sit9531x_dpll_pin {
+ struct list_head list;
+ struct sit9531x_dpll *dpll;
+ struct dpll_pin *dpll_pin;
+ dpll_tracker tracker;
+ struct fwnode_handle *fwnode;
+ char label[8]; /* "IN0", "OUT3" */
+ enum dpll_pin_direction dir;
+ u8 id; /* hardware index */
+ u8 prio;
+ enum dpll_pin_state pin_state;
+ bool seen; /* baseline taken by the poll */
+};
+
+/* Per-PLL DPLL device state. */
+struct sit9531x_dpll {
+ struct list_head list;
+ struct sit9531x_dev *dev;
+ struct dpll_device *dpll_dev;
+ dpll_tracker tracker;
+ struct dpll_device_ops ops; /* per-instance copy */
+ struct list_head pins;
+ u8 id; /* 0 = PLLA .. 3 = PLLD */
+ enum dpll_lock_status lock_status;
+ enum dpll_lock_status_error lock_status_error;
+};
+
+/* ---- DPLL allocation and registration ---- */
+/*
+ * The callback tables stay with the callbacks; the registration code that
+ * hands them to the subsystem lives next to probe() in core.c.
+ */
+extern const struct dpll_device_ops sit9531x_dpll_device_ops;
+const struct dpll_pin_ops *
+sit9531x_dpll_pin_ops_get(const struct sit9531x_dpll_pin *pin);
+
+struct sit9531x_dpll *sit9531x_dpll_alloc(struct sit9531x_dev *sitdev, u8 ch);
+void sit9531x_dpll_free(struct sit9531x_dpll *sitdpll);
+int sit9531x_dpll_register(struct sit9531x_dpll *sitdpll);
+void sit9531x_dpll_unregister(struct sit9531x_dpll *sitdpll);
+
+/* ---- Periodic change detection ---- */
+void sit9531x_dpll_changes_check(struct sit9531x_dpll *sitdpll);
+
+#endif /* _SIT9531X_DPLL_H */
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index 67077d112653..9349ec722569 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -37,7 +37,264 @@
*/
#define SIT9531X_REG_VARIANT_ID SIT9531X_REG(0x00, 0x02)
-/* Variant ID values (page 0 reg 0x02) */
+/* DCO trigger pulse timing: minimum 6 ns required by hardware */
+
+/* Page 0 -- PLL inner loop loss-of-lock */
+#define SIT9531X_REG_PLL_INNER_LOL_STATUS SIT9531X_REG(0x00, 0x92)
+#define SIT9531X_REG_PLL_INNER_LOL_NOTIF SIT9531X_REG(0x00, 0x93)
+
+/* Page 0 -- Clock monitor PLL / XO status */
+#define SIT9531X_REG_CMON_NOTIF SIT9531X_REG(0x00, 0x9E)
+
+/* Page 0 -- PLL outer-loop loss-of-lock */
+#define SIT9531X_REG_OUTER_LOL_STATUS SIT9531X_REG(0x00, 0x06)
+#define SIT9531X_REG_OUTER_LOL_NOTIF SIT9531X_REG(0x00, 0x07)
+
+/* Page 0 -- PLL holdover freeze status */
+#define SIT9531X_REG_HO_FREEZE_STATUS SIT9531X_REG(0x00, 0x0A)
+#define SIT9531X_REG_HO_FREEZE_NOTIF SIT9531X_REG(0x00, 0x0B)
+
+/* Page 0 -- INTSYNC (inter-PLL synchronization) global enable */
+#define SIT9531X_REG_INTSYNC_GLOBAL SIT9531X_REG(0x00, 0x40)
+#define SIT9531X_INTSYNC_EN_BIT 6
+
+/*
+ * Priority table: 6 registers per PLL, each holds two priority slots
+ * nibble-packed. The register holding slots 2n and 2n+1 keeps the
+ * earlier slot (CLK_SPARE<2n>SEL_PLL) in [7:4] and the later one in
+ * [3:0].
+ *
+ * Base registers for PLLA: 0x16-0x1B (slots 0-10 plus the
+ * active-reference nibble).
+ * For PLL N: base + 6 * N (e.g. PLLB starts at 0x1C).
+ *
+ * Input source encoding (4-bit value):
+ * 0=IN0P, 1=IN1P, 2=IN2P, 3=IN3P,
+ * 5=OCXO, 6=INTSYNC,
+ * 7=IN0N, 8=IN1N, 9=IN2N, 10=IN3N
+ *
+ * Codes 4 and 11 address a fifth input pair that this part does
+ * not have. They read back as no valid reference.
+ */
+#define SIT9531X_PAGE_PRIOSYS 0x01
+#define SIT9531X_PRIO_BASE_REG 0x16
+#define SIT9531X_PRIO_REGS_PER_PLL 6
+#define SIT9531X_PRIO_SLOTS_PER_REG 2
+/*
+ * 11 priority slots, CLK_SPARE0SEL_PLL through CLK_SPARE10SEL_PLL.
+ * The twelfth nibble of the block is not a slot: it is
+ * CLK_ACTIVESEL_PLL, see SIT9531X_PRIO_ACTIVESEL_OFF below.
+ */
+#define SIT9531X_PRIO_MAX_SLOTS 11
+/* Number of source encodings (0-11), unrelated to the slot count */
+#define SIT9531X_PRIO_NUM_SRC 12
+#define SIT9531X_PRIO_NIBBLE_MASK 0x0F
+#define SIT9531X_PRIO_HI_SHIFT 4
+/* Input source encoding values (see table above) */
+/*
+ * Either of the two codes for the absent fifth pair works as "no source";
+ * the driver writes this one when it empties a slot.
+ */
+#define SIT9531X_PRIO_SRC_NONE 4
+#define SIT9531X_PRIO_SRC_OCXO 5
+#define SIT9531X_PRIO_SRC_INTSYNC 6
+#define SIT9531X_PRIO_SRC_N_BASE 7
+/*
+ * The last register of each PLL's priority block holds, in its low
+ * nibble, the input source the PLL has currently selected as its
+ * active reference (CLK_ACTIVESEL_PLL, same 4-bit encoding as above).
+ */
+#define SIT9531X_PRIO_ACTIVESEL_OFF 5
+
+/*
+ * Page 0 -- PRG_Directives_GENERIC_0, the main system's programming
+ * directive register. Every page carries its own copy of this
+ * register at offset 0x0F with the same bit layout:
+ *
+ * bit 6 proceed to loop lock / active state from the PRG_CMD state
+ * bit 4 update the NVM bank from the efuse contents
+ * bit 3 read the efuse into the volatile registers
+ * bit 2 program the efuse
+ * bit 1 small change update (SIT9531X_SMALL_UPDATE_CMD)
+ * bit 0 escape to the PRG_CMD state
+ *
+ * The NVM bank is a volatile shadow, so bits 4 and 1 are both fine in
+ * a runtime path: bit 1 for a change made in the active state, bit 4
+ * to close a PRG_CMD sequence. Only bit 2 writes non-volatile
+ * storage, and the driver never issues it.
+ */
+#define SIT9531X_REG_GLOBAL_UPDATE SIT9531X_REG(0x00, 0x0F)
+#define SIT9531X_SMALL_UPDATE_CMD 0x02
+
+/* One bit per input PAIR (bit 0 = CLKIN0, ..., bit 3 = CLKIN3) */
+#define SIT9531X_REG_IN_DE_FORCE SIT9531X_REG(0x02, 0xE8)
+#define SIT9531X_REG_IN_DE_STATE SIT9531X_REG(0x02, 0xE9)
+#define SIT9531X_REG_IN_SEP_FORCE SIT9531X_REG(0x02, 0xEA)
+#define SIT9531X_REG_IN_SEP_STATE SIT9531X_REG(0x02, 0xEB)
+#define SIT9531X_REG_IN_SEN_FORCE SIT9531X_REG(0x02, 0xF2)
+#define SIT9531X_REG_IN_SEN_STATE SIT9531X_REG(0x02, 0xF3)
+
+/*
+ * One register per input pair at 0x1B + 0x10 * pair
+ * (CLKIN0 = 0x1B, CLKIN1 = 0x2B, CLKIN2 = 0x3B, CLKIN3 = 0x4B).
+ * SE_P_EN/SE_N_EN set means the corresponding lane is configured
+ * single-ended; both clear means the pair runs differential.
+ */
+#define SIT9531X_REG_IN_MODE(_pair) \
+ SIT9531X_REG(0x02, 0x1B + 0x10 * (_pair))
+#define SIT9531X_IN_MODE_SE_P_EN BIT(0)
+#define SIT9531X_IN_MODE_SE_N_EN BIT(1)
+
+/* ---- Page 0x03 (Output System) registers -- Hi-Z control ---- */
+#define SIT9531X_REG_HIZ_DIFF_07_MASK SIT9531X_REG(0x03, 0xF2)
+#define SIT9531X_REG_HIZ_DIFF_07_STATE SIT9531X_REG(0x03, 0xF3)
+#define SIT9531X_REG_HIZ_DIFF_811_MASK SIT9531X_REG(0x03, 0xF4)
+#define SIT9531X_REG_HIZ_DIFF_811_STATE SIT9531X_REG(0x03, 0xF5)
+#define SIT9531X_REG_HIZ_SE_07_MASK SIT9531X_REG(0x03, 0xF8)
+#define SIT9531X_REG_HIZ_SE_07_STATE SIT9531X_REG(0x03, 0xF9)
+#define SIT9531X_REG_HIZ_SE_811_MASK SIT9531X_REG(0x03, 0xFA)
+#define SIT9531X_REG_HIZ_SE_811_STATE SIT9531X_REG(0x03, 0xFB)
+/*
+ * The SE pairs above force the OutP pad of a CMOS output. The OutN pad
+ * has its own: 0xF6/0xF7 for slots 0-7, and for slots 8-11 the upper
+ * nibble of the differential pair, 0xF4/0xF5 bits 7:4.
+ */
+#define SIT9531X_REG_HIZ_SEN_07_MASK SIT9531X_REG(0x03, 0xF6)
+#define SIT9531X_REG_HIZ_SEN_07_STATE SIT9531X_REG(0x03, 0xF7)
+#define SIT9531X_HIZ_SEN_811_SHIFT 4
+
+/*
+ * Output driver configuration. Either CMOS enable means the output
+ * is wired single-ended -- one lane, or both driven as CMOS; with
+ * neither set it is a differential pair.
+ */
+#define SIT9531X_OUT_MISC0_BASE 0x1E
+#define SIT9531X_OUT_MISC0_STRIDE 0x10
+#define SIT9531X_OUT_CMOS_ENP BIT(3)
+#define SIT9531X_OUT_CMOS_ENN BIT(2)
+
+/*
+ * Output divider registers in Pages 3/4. Each output has a 34-bit
+ * integer divider mapped to 5 bytes (LSB at base reg, MSB at base-4).
+ * Outputs 0-5 are on Page 3, outputs 6-11 are on Page 4.
+ *
+ * The base register for slot N within a page is:
+ * clkout_odr_divn_base[slot] = { 0x14, 0x24, 0x34, 0x44, 0x54, 0x64 }
+ *
+ * Layout: base=LSB, base-1, base-2, base-3, base-4[1:0]=MSB.
+ *
+ * Per-chip clkout_map[] translates output index to slot position.
+ */
+#define SIT9531X_PAGE_OUTSYS0_SLOT_MAX 5 /* slots 0-5 on Page 0x03 */
+
+/* Misc output system registers */
+#define SIT9531X_REG_PRG_DIR_GEN SIT9531X_REG(0x03, 0x0F)
+#define SIT9531X_PRG_CMD_STATE 0x01
+#define SIT9531X_UPDATE_NVM 0x10
+#define SIT9531X_LOOP_LOCK 0x40
+
+/* Debug register (same offset, per-page) */
+#define SIT9531X_REG_OUTSYS_DEBUG SIT9531X_REG(0x03, 0xBD)
+#define SIT9531X_DEBUG_UNLOCK_VAL 0xC3
+
+/*
+ * On-demand phase-flush fired from a register rather than a GPIO pin.
+ * DIVO_PHASE_SEL_REG selects the in-register trigger source and
+ * DIVO_PHASE_TRIG flushes the output phase when pulsed high then low.
+ * The unrelated OEb trigger pair in bits [7:6] must be preserved.
+ */
+#define SIT9531X_REG_GPIO_FUNC_CTRL1 SIT9531X_REG(0x00, 0x65)
+#define SIT9531X_DIVO_PHASE_SEL_REG BIT(5)
+#define SIT9531X_DIVO_PHASE_TRIG BIT(4)
+
+/* ---- PLL page registers (apply to pages 0x0A-0x0D) ---- */
+#define SIT9531X_PLL_REG_SMALL_UPDATE 0x0F
+
+/*
+ * Loop-filter coefficients on PLL_PAGE regs 0x10-0x15 (3 normal +
+ * 3 fast-lock) are GUI/NVM-generated by the timing configurator and must not be
+ * reprogrammed at runtime; the register map flags them as
+ * "GUI generated configuration should not change manually".
+ */
+
+/*
+ * Output-enable mask, twelve bits: bits 0-7 in LO, bits 8-11 in HI[3:0].
+ * OUTn is bit n on PLLA and PLLB and bit 11 - n on PLLC and PLLD.
+ */
+#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_ACTIVE 0x02
+#define SIT9531X_PLL_ACTIVE_BIT BIT(0) /* PLL reached active state */
+
+#define SIT9531X_PLL_STATUS_OUTER_DIS BIT(5)
+
+/*
+ * PLL_CONFIG1F_PLL: bit 6 puts the PLL in manual active select, where it
+ * no longer arbitrates by priority. The other bits are GUI-generated
+ * configuration.
+ */
+#define SIT9531X_PLL_REG_CONFIG1F 0x1F
+#define SIT9531X_PLL_CONFIG1F_MANUAL_SEL BIT(6)
+
+/*
+ * MISCINNER_PLL: in manual active select, bit 5 makes the PLL follow the
+ * manual input select instead of its active selection. The other bits
+ * are GUI-generated configuration.
+ */
+#define SIT9531X_PLL_REG_MISCINNER 0x18
+#define SIT9531X_PLL_MISCINNER_MAN_IN_SEL BIT(5)
+
+/*
+ * GPIO_INPUT_FUNC_CTRL5..8 (page 0), one per PLL: the manual input
+ * select. With bit 4 set the input is the low nibble of this register,
+ * otherwise it comes from the input-select pins. Bits 6:5 are the
+ * active/spare choice of an indirect selection (bit 6 enables bit 5,
+ * which picks the active or the spare clock) and are not decoded here.
+ */
+#define SIT9531X_REG_MAN_IN_SEL(_pll) SIT9531X_REG(0x00, 0xE8 + (_pll))
+#define SIT9531X_MAN_IN_SEL_FROM_REG BIT(4)
+#define SIT9531X_MAN_IN_SEL_MASK GENMASK(3, 0)
+
+/*
+ * Per-PLL status register. HO_VALID says the holdover window holds a
+ * valid frequency estimate, i.e. holdover memory has been acquired; it is
+ * not the same as HO_FREEZE (page 0, reg 0x0A), which says the PLL has
+ * already switched over to holdover.
+ */
+#define SIT9531X_PLL_REG_STATUS_1 0x06
+#define SIT9531X_PLL_STATUS_1_HO_VALID BIT(2)
+
+#define SIT9531X_CLKMON_P_NOTIF_01 SIT9531X_REG(0x06, 0x03)
+#define SIT9531X_CLKMON_P_NOTIF_23 SIT9531X_REG(0x06, 0x07)
+
+#define SIT9531X_CLKMON_N_NOTIF_01 SIT9531X_REG(0x06, 0x93)
+#define SIT9531X_CLKMON_N_NOTIF_23 SIT9531X_REG(0x06, 0x97)
+
+/* Per-input bit offsets within clock monitor nibble */
+
+/*
+ * EEPROM profile load, page 0. The device compares the CRC stored in the
+ * EEPROM against the one it computes from what it read; a mismatch means
+ * the profile on the part is not the profile the board expects.
+ * NOTIFY_4 collects the read-done bit and the defect bits, all sticky, so
+ * a healthy load leaves exactly the read-done bit set.
+ */
+/* 4 bytes each, MSB first */
+#define SIT9531X_REG_REC_CRC SIT9531X_REG(0x00, 0x8A)
+#define SIT9531X_REG_CAL_CRC SIT9531X_REG(0x00, 0x8E)
+#define SIT9531X_REG_EEPROM_NOTIF SIT9531X_REG(0x00, 0x97)
+#define SIT9531X_EEPROM_READ_DONE BIT(0)
+
+/* Profile identifier the loaded configuration carries, page 1, 24 bits */
+/* 3 bytes, LSB first */
+#define SIT9531X_REG_PROFILE_ID SIT9531X_REG(0x01, 0x44)
+
+/* Consecutive failed INTRB acknowledgements before the line is given up */
+#define SIT9531X_IRQ_ACK_TRIES 8
+
+/* ---- Variant ID values (one byte at SIT9531X_REG_VARIANT_ID) ---- */
#define SIT9531X_VARIANT_ID_95317 0x17
#define SIT9531X_VARIANT_ID_95316 0x31
--
2.43.0
^ permalink raw reply [flat|nested] 26+ messages in thread* Re: [PATCH net-next v11 06/13] dpll: sit9531x: register DPLL devices and pins
2026-09-30 23:37 ` [PATCH net-next v11 06/13] dpll: sit9531x: register DPLL devices and pins Ali Rouhi
@ 2026-10-05 1:09 ` netdev-bot+sashiko
0 siblings, 0 replies; 26+ messages in thread
From: netdev-bot+sashiko @ 2026-10-05 1:09 UTC (permalink / raw)
To: arouhi
Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba,
pabeni, robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
devicetree, netdev, linux-kernel
Thank you for your contribution! Sashiko AI review found 4 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 2 · Low: 2
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] sit9531x_dpll_mode_get() always returns DPLL_MODE_AUTOMATIC,
and sit9531x_dpll_supported_modes_get() advertises only AUTOMATIC.
- [Medium] sit9531x_dpll_lock_status_get() tests chan->locked before
chan->ho_freeze, so a PLL frozen in holdover whose outer-LOL bit is…
- [Low] sit9531x_dpll_changes_check() takes a first-poll baseline for
pins (pin->seen) but not for the device lock status.
- [Low] Each 500 ms tick does roughly 100+ single-byte I2C transfers plus
page-selector writes.
--
dpll: sit9531x: register DPLL devices and pins
This patch registers each of the four on-chip SiT9531x PLLs as a DPLL
device and adds the crystal as a permanently connected pin on each. The
lock status and mode callbacks read from a state cache that a kthread
worker refreshes every 500 ms; the INTRB interrupt can trigger a refresh
early, and the same worker sends the change notifications.
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index c7c14442df922..bf6817b9d5656 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
[ ... ]
> +/*
> + * sit9531x_prio_table_read - read a PLL's priority-table source codes
> + * @srcs: output array of SIT9531X_PRIO_MAX_SLOTS source codes
> + *
> + * Caller must hold sitdev->multiop_lock.
> + */
> +static int sit9531x_prio_table_read(struct sit9531x_dev *sitdev, u8 pll_idx,
> + u8 *srcs)
> +{
> + u8 val, slot;
> + int rc;
> +
> + for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
> + rc = sit9531x_read_u8(sitdev,
> + sit9531x_prio_reg(pll_idx, slot), &val);
> + if (rc)
> + return rc;
> +
> + srcs[slot] = sit9531x_prio_slot_get(val, slot);
> + }
> +
> + return 0;
> +}
[Severity: Low]
Slots 2n and 2n+1 share one register, so sit9531x_prio_reg() returns the
same address on two consecutive iterations. Is it intended that 5 of the
6 priority registers are read twice, for every PLL, on every poll?
sit9531x_chan_state_fetch() calls this once per PLL from the 500 ms poll.
It also re-reads the chip-global page 0 bytes for each of the four PLLs:
rc = sit9531x_read_u8(sitdev, SIT9531X_REG_OUTER_LOL_STATUS,
&outer_lol);
...
rc = sit9531x_read_u8(sitdev, SIT9531X_REG_PLL_INNER_LOL_STATUS,
&inner_lol);
...
rc = sit9531x_read_u8(sitdev, SIT9531X_REG_HO_FREEZE_STATUS,
&ho_freeze);
Add the per-input force/state reads and the 8 W1C writes in
sit9531x_clear_notifications(), and each tick comes to roughly 100 or
more single-byte transfers plus page-selector writes. These move between
the PLL page, page 0 and page 1.
At 100 kHz that keeps a bus, which may be shared, busy about 10% of the
time. The input-monitor reads added later in the series raise this
further.
Could each priority register be read once, and the global status bytes
once per tick? That would remove about 30 transfers per tick.
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> new file mode 100644
> index 0000000000000..924386aec4d88
> --- /dev/null
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -0,0 +1,389 @@
[ ... ]
> +sit9531x_dpll_lock_status_get(const struct dpll_device *dpll, void *dpll_priv,
[ ... ]
> + if (!chan->active) {
[ ... ]
> + } else if (chan->inner_lol) {
[ ... ]
> + *status = chan->ho_freeze ? DPLL_LOCK_STATUS_HOLDOVER :
> + DPLL_LOCK_STATUS_UNLOCKED;
> + } else if (chan->mode) {
[ ... ]
> + *status = DPLL_LOCK_STATUS_UNLOCKED;
> + } else if (chan->locked) {
> + /*
> + * HO_ACQ is locked *and* holdover memory acquired, so it needs
> + * the holdover-valid bit rather than following from the lock.
> + */
> + if (chan->ho_valid)
> + *status = DPLL_LOCK_STATUS_LOCKED_HO_ACQ;
> + else
> + *status = DPLL_LOCK_STATUS_LOCKED;
> + } else if (chan->ho_freeze) {
> + *status = DPLL_LOCK_STATUS_HOLDOVER;
> + } else {
> + *status = DPLL_LOCK_STATUS_UNLOCKED;
> + }
[Severity: Medium]
Can a PLL that is frozen in holdover be reported as LOCKED here?
chan->locked is tested before chan->ho_freeze, and
sit9531x_chan_state_fetch() reads the two independently:
chan->locked = active && !(outer_lol & BIT(pll_idx));
...
chan->ho_freeze = !!(ho_freeze & BIT(pll_idx));
If the holdover freeze bit is set while the outer LOL bit is still
clear, this returns LOCKED or LOCKED_HO_ACQ instead of HOLDOVER.
The pin-state contract later in this file treats that combination as
possible: L is "chan->locked && !chan->mode && !chan->ho_freeze". The
inner_lol branch above also checks ho_freeze first, so the two branches
use different precedence.
Later in the series, sit9531x_prio_table_commit() forces holdover through
PLL reg 0x6F bit 4. The "implement input pin state on a DPLL" patch also
leaves a PLL in forced holdover indefinitely when its last input is
removed.
If outer LOL stays clear during forced holdover, would the device report
a lock while no pin reports ACTIVE? The ordering is still the same at the
end of the series.
Should the ho_freeze test come before the locked test?
> +static int
> +sit9531x_dpll_mode_get(const struct dpll_device *dpll, void *dpll_priv,
> + enum dpll_mode *mode, struct netlink_ext_ack *extack)
> +{
> + *mode = DPLL_MODE_AUTOMATIC;
> +
> + return 0;
> +}
> +
> +static int
> +sit9531x_dpll_supported_modes_get(const struct dpll_device *dpll,
> + void *dpll_priv, unsigned long *modes,
> + struct netlink_ext_ack *extack)
> +{
> + __set_bit(DPLL_MODE_AUTOMATIC, modes);
> +
> + return 0;
> +}
[Severity: Medium]
Is DPLL_MODE_AUTOMATIC the right value for a PLL whose profile sets
manual active select? sit9531x_manual_sel_report() detects PLL_CONFIG1F
bit 6 at probe, but only warns:
"PLL%c: the profile selects its reference manually, ... it is
reported as automatic\n"
The uAPI defines the reported value as:
* @DPLL_MODE_AUTOMATIC: highest prio input pin auto selected by dpll
On those boards userspace is told the PLL picks the highest-priority
input, but the PLL actually follows a fixed manual selection.
With no .mode_set, the core already refuses mode changes. For a PLL where
sit9531x_manual_sel_report() found bit 6 set, could mode_get report
DPLL_MODE_MANUAL and supported_modes_get list only MANUAL?
The commit message explains the choice but does not compare it with this
read-only option. The behaviour is the same at the end of the series.
[ ... ]
> + if (sitdpll->lock_status != lock_status ||
> + sitdpll->lock_status_error != status_error) {
> + sitdpll->lock_status = lock_status;
> + sitdpll->lock_status_error = status_error;
> + dpll_device_change_ntf(sitdpll->dpll_dev);
> + }
[Severity: Low]
The pin loop below takes a first-poll baseline through pin->seen, but
this device-level comparison has no baseline. sit9531x_dpll_alloc()
starts the cached value as a placeholder:
sitdpll->lock_status = DPLL_LOCK_STATUS_UNLOCKED;
sit9531x_dev_start() registers the DPLLs, so the create notification
already carries the real state from sit9531x_dev_state_fetch(). It then
queues the worker with delay 0.
Won't the first tick send a dpll_device_change_ntf() for every PLL that
is locked or in holdover, even though nothing changed? That seems to
contradict "the first tick takes the baseline" in the commit message.
This is the same at the end of the series.
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260930233714.87679-1-arouhi%40sitime.com
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH net-next v11 07/13] dpll: sit9531x: implement input pin state on a DPLL
2026-09-30 23:37 [PATCH net-next v11 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (5 preceding siblings ...)
2026-09-30 23:37 ` [PATCH net-next v11 06/13] dpll: sit9531x: register DPLL devices and pins Ali Rouhi
@ 2026-09-30 23:37 ` Ali Rouhi
2026-10-05 1:10 ` netdev-bot+sashiko
2026-09-30 23:37 ` [PATCH net-next v11 08/13] dpll: sit9531x: add support to get and set priority on input pins Ali Rouhi
` (5 subsequent siblings)
12 siblings, 1 reply; 26+ messages in thread
From: Ali Rouhi @ 2026-09-30 23:37 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi, devicetree,
netdev, linux-kernel
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Report and set whether an input takes part in a PLL's reference
selection. The device selects its reference itself from a priority
table, so membership of that table is what the state means here: an input
in the table is selectable, one absent from it is disconnected. Setting
the state adds the input to the table or removes it, and membership is
read back from the device rather than remembered. CONNECTED would ask
for this input and no other, which the device cannot be told to do, so it
is refused. A physical input's receiver is shared by the four PLLs and is
powered down only when the last of them lets go of it.
What the device does with an input is its operational state. The PLL's
active selection names one source, and that pin is active while the PLL
is locked to it -- outer loop running, not frozen -- and its lane has
signal. The poll reads every lane's clock monitor: an input whose
monitor reports loss of signal is no-signal, one that reports a frequency
drift is qual-failed, any other is standby. A selection naming a lane
without signal reports no pin active: the device has then fallen back to
another listed source on its own, and this driver does not read which.
The table is built from a priority the driver keeps per source and PLL,
seeded from the table the device loaded: members in priority order, the
slots past the last one naming no source. The four-bit slot encoding
has two codes for a fifth input pair this part does not have, and one of
them serves as that. A disconnected input keeps its priority for when it
comes back, and a table that would come out as the device already holds
it is not written.
The last register of the table also carries the device's active
selection. After a table write the PLL goes to the source it names, and
leaves it on its own only when that source loses its signal, so every
write names a source the PLL can use. When the priorities put a
different source with signal first, the selection goes to it -- the
highest-priority valid input, which is how the DPLL interface defines
automatic mode. A write that only changes the table below that source
leaves the selection where it is while it is still listed and has
signal, so an edit there does not pull a PLL off a healthy reference;
failing that, it goes to the first listed source that has signal.
Removing the last input is allowed. The selection then has nothing to
name, and left as it is it would keep the PLL following its old source
for as long as that has signal, so the PLL stays in the forced holdover
the write sequence uses -- the one state in which it follows no input.
The next write that lists a source releases it.
The pins fall into three roles and only the first answers this question,
so the contract for all of them is written above the operations rather
than left to be inferred: inputs and the inter-PLL sync destination take
part in selection; an output and the sync source are driven by the PLL and
report whether they carry a signal; the crystal is fixed and always
connected, since a PLL cannot be told to stop using it.
Probe also warns about a firmware pin node whose reg names no pin the
device can have -- a lane or an output the variant lacks, or the N lane
of a pair the configuration runs differential -- since nodes are looked
up from the pins and such a node would otherwise be dropped silently.
Rewriting the table means forcing the PLL into holdover, waiting for it to
take, writing every slot and releasing holdover again, all under the
device lock -- ten to twenty milliseconds. The device has no way to
change one slot in isolation, and holdover is what keeps the loop from
chasing a table that is momentarily inconsistent, so the sequence is the
cost of doing it correctly.
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
drivers/dpll/sit9531x/core.c | 922 ++++++++++++++++++++++++++++++++++-
drivers/dpll/sit9531x/core.h | 25 +-
drivers/dpll/sit9531x/dpll.c | 274 +++++++++++
drivers/dpll/sit9531x/regs.h | 16 +
4 files changed, 1215 insertions(+), 22 deletions(-)
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index bf6817b9d565..231e2d71dd6f 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -239,6 +239,99 @@ static void sit9531x_input_get_regs(const struct sit9531x_dev *sitdev,
}
}
+/*
+ * sit9531x_input_disable - disable an input reference
+ * @index: logical input index (0-N)
+ *
+ * Sets the force mask bit and clears the state bit for the given
+ * input, effectively disabling it. Register selection depends on
+ * the pair's signal mode (SE/DE) and the lane (P/N); the bit within
+ * each register addresses the input pair.
+ *
+ * Both writes are attempted even when the first fails, and the first
+ * error is returned. Neither is rolled back: the force and state bits
+ * only mean something together, so a transient bus error can leave the
+ * force bit asserted over a state bit that was never programmed, and the
+ * error is what says the override is not to be trusted.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_input_disable(struct sit9531x_dev *sitdev, u8 index)
+{
+ unsigned int force_reg, state_reg;
+ struct sit9531x_ref *ref;
+ u8 pair, val;
+ int rc, ret;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (index >= SIT9531X_MAX_INPUTS)
+ return -EINVAL;
+
+ ref = &sitdev->ref[index];
+ pair = sit9531x_input_pair(index);
+ sit9531x_input_get_regs(sitdev, index, &force_reg, &state_reg);
+
+ rc = sit9531x_read_u8(sitdev, force_reg, &val);
+ if (!rc)
+ rc = sit9531x_write_u8(sitdev, force_reg, val | BIT(pair));
+
+ ret = sit9531x_read_u8(sitdev, state_reg, &val);
+ if (!ret)
+ ret = sit9531x_write_u8(sitdev, state_reg, val & ~BIT(pair));
+ if (ret && !rc)
+ rc = ret;
+
+ /*
+ * Drop the claim even when the pair could not be programmed. The
+ * force bit may be asserted over a state bit that never reached the
+ * device, so the receiver cannot be relied on; leaving the cache
+ * saying it is on makes the next enable skip itself and report a
+ * success the signal does not back. Clearing the force bit again
+ * is harmless, so the worst this costs is one redundant write.
+ */
+ ref->enabled = false;
+
+ return rc;
+}
+
+/*
+ * sit9531x_input_enable - enable an input reference
+ * @index: logical input index (0-N)
+ *
+ * Clears the force mask bit for the given input, returning it to
+ * hardware default (enabled).
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_input_enable(struct sit9531x_dev *sitdev, u8 index)
+{
+ unsigned int force_reg, state_reg;
+ struct sit9531x_ref *ref;
+ u8 pair, val;
+ int rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (index >= SIT9531X_MAX_INPUTS)
+ return -EINVAL;
+
+ ref = &sitdev->ref[index];
+ pair = sit9531x_input_pair(index);
+ sit9531x_input_get_regs(sitdev, index, &force_reg, &state_reg);
+
+ rc = sit9531x_read_u8(sitdev, force_reg, &val);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, force_reg, val & ~BIT(pair));
+ if (rc)
+ return rc;
+
+ ref->enabled = true;
+
+ return 0;
+}
+
/*
* Output enable / disable (Hi-Z control)
*
@@ -433,26 +526,537 @@ static u8 sit9531x_prio_slot_get(u8 val, u8 slot)
return val >> SIT9531X_PRIO_HI_SHIFT;
}
+/* Place source @src in priority slot @slot of a register value. */
+static u8 sit9531x_prio_slot_set(u8 val, u8 slot, u8 src)
+{
+ if (slot & 1)
+ return (val & (SIT9531X_PRIO_NIBBLE_MASK <<
+ SIT9531X_PRIO_HI_SHIFT)) |
+ (src & SIT9531X_PRIO_NIBBLE_MASK);
+
+ return (val & SIT9531X_PRIO_NIBBLE_MASK) |
+ ((src & SIT9531X_PRIO_NIBBLE_MASK) <<
+ SIT9531X_PRIO_HI_SHIFT);
+}
+
+/*
+ * Commit a priority-table programming sequence through the Page-0
+ * programming directive register.
+ *
+ * A small change update is all the table needs. The NVM-bank and
+ * loop-lock directives that the output system issues do not belong
+ * here: the former programs non-volatile storage from the efuse and
+ * the latter only means anything after an escape to the PRG_CMD
+ * state. This matches the documented input_priority_sel() procedure.
+ */
+static int sit9531x_prio_prg_commit(struct sit9531x_dev *sitdev)
+{
+ int rc;
+
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+ SIT9531X_SMALL_UPDATE_CMD);
+ if (rc)
+ return rc;
+
+ usleep_range(1000, 2000);
+
+ return 0;
+}
+
+/*
+ * sit9531x_input_mon_fetch - read the clock monitor status of every lane
+ *
+ * Four registers carry a nibble per lane. Loss of signal and frequency
+ * drift are what separate an input the device could lock to from one it
+ * could not, which the priority commit uses to choose the active
+ * selection and the pins report as their operational state.
+ *
+ * Caller must hold sitdev->multiop_lock, or run before registration.
+ */
+static int sit9531x_input_mon_fetch(struct sit9531x_dev *sitdev)
+{
+ static const unsigned int regs[] = {
+ SIT9531X_CLKMON_P_STATUS_01, SIT9531X_CLKMON_P_STATUS_23,
+ SIT9531X_CLKMON_N_STATUS_01, SIT9531X_CLKMON_N_STATUS_23,
+ };
+ u8 val[ARRAY_SIZE(regs)], i, pair, nib;
+ int rc;
+
+ for (i = 0; i < ARRAY_SIZE(regs); i++) {
+ rc = sit9531x_read_u8(sitdev, regs[i], &val[i]);
+ if (rc)
+ return rc;
+ }
+
+ for (i = 0; i < sitdev->info->num_inputs; i++) {
+ struct sit9531x_ref *ref = &sitdev->ref[i];
+
+ /* Lane 2k is INkP, 2k + 1 INkN; a register holds two pairs. */
+ pair = sit9531x_input_pair(i);
+ nib = val[(sit9531x_input_is_n(i) ? 2 : 0) + pair / 2];
+ nib = (pair & 1) ? nib >> 4 : nib & 0x0F;
+
+ ref->los = !!(nib & SIT9531X_CLKMON_LOSS);
+ ref->qual_fail = !ref->los &&
+ !!(nib & (SIT9531X_CLKMON_FINE_DRIFT |
+ SIT9531X_CLKMON_COARSE_DRIFT));
+ }
+
+ return 0;
+}
+
/*
- * Rebuild a PLL's membership mask from the source codes of its priority
- * table. The mask is what the pin state getters test, so it is refreshed
- * from exactly the values the table holds -- here after a write, and once
- * per poll from the read-back in sit9531x_chan_state_fetch().
+ * Can the device lock to this source now? Only the input lanes have a
+ * monitor; the on-chip oscillator and the inter-PLL net are taken as
+ * present. A differential pair is watched through its P lane, which is
+ * the one its table entries are canonicalised to.
+ */
+static bool sit9531x_prio_src_live(const struct sit9531x_dev *sitdev, u8 src)
+{
+ u8 index = sit9531x_hw_src_input(src);
+
+ if (index >= sitdev->info->num_inputs)
+ return true;
+
+ return !sitdev->ref[index].los;
+}
+
+/*
+ * Fold a source code to the lane a DPLL pin actually represents.
+ *
+ * Differential input pairs expose only the P lane as a DPLL pin. A
+ * priority table entry encoded as an N lane for such a pair must map to
+ * the P-lane source for pin-facing operations (membership, priority slots,
+ * add/remove/set lookups), matching sit9531x_ref_pll_mask_fetch().
+ */
+static u8 sit9531x_prio_src_canon(const struct sit9531x_dev *sitdev, u8 src)
+{
+ u8 index = sit9531x_hw_src_input(src);
+
+ if (index >= sitdev->info->num_inputs)
+ return src;
+
+ if (sit9531x_input_is_n(index) &&
+ sitdev->ref[index].sig_mode == SIT9531X_MODE_DE)
+ return sit9531x_input_hw_src(index - 1);
+
+ return src;
+}
+
+/*
+ * sit9531x_input_prio_present - is a source listed in a PLL's priority table
+ * @input_idx: input source in hardware encoding (see
+ * sit9531x_input_hw_src())
+ *
+ * Answers from the membership mask that every table write and every poll
+ * refreshes, which is what the pin state getters test. The priority slot
+ * cannot answer this: a source that is not in the table reports the lowest
+ * slot, so the slot value alone does not separate absent from last.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+bool sit9531x_input_prio_present(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 input_idx)
+{
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return false;
+
+ input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+ if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+ return false;
+
+ return !!(sitdev->chan[pll_idx].prio_mask & BIT(input_idx));
+}
+
+/*
+ * Take the configured priorities from a table the hardware holds: each
+ * listed source gets the first slot it occupies. A source the table does
+ * not list keeps whatever it had, so a disconnected input comes back with
+ * its old priority.
+ */
+static void sit9531x_prio_cfg_seed(struct sit9531x_dev *sitdev, u8 pll_idx,
+ const u8 *srcs)
+{
+ struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+ u16 seeded = 0;
+ u8 slot, src;
+
+ for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+ src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
+ if (!sit9531x_prio_src_usable(src))
+ continue;
+ src = sit9531x_prio_src_canon(sitdev, src);
+ if (seeded & BIT(src))
+ continue;
+ seeded |= BIT(src);
+ chan->cfg_prio[src] = slot;
+ chan->cfg_known |= BIT(src);
+ }
+
+ /*
+ * A source this PLL has never listed gets the lowest slot, the value
+ * it reports and the one it is connected at, so its priority does not
+ * change when its state does.
+ */
+ for (src = 0; src < SIT9531X_PRIO_NUM_SRC; src++) {
+ if (chan->cfg_known & BIT(src))
+ continue;
+ chan->cfg_prio[src] = SIT9531X_PRIO_MAX_SLOTS - 1;
+ chan->cfg_known |= BIT(src);
+ }
+
+ memcpy(chan->seen_srcs, srcs, sizeof(chan->seen_srcs));
+ chan->seen_valid = true;
+}
+
+/*
+ * Build the table for a set of member sources: ordered by configured
+ * priority, ties kept in the order the hardware table has them, the slots
+ * past the last member naming no source. Filling them with the code for
+ * no source rather than with copies of the last member keeps every
+ * source in exactly one slot, so the order the table encodes is the
+ * order the priorities say.
+ */
+static void sit9531x_prio_table_build(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u16 members, u8 *srcs)
+{
+ const struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+ u8 order[SIT9531X_PRIO_NUM_SRC], n = 0, i, j, src;
+
+ for (src = 0; src < SIT9531X_PRIO_NUM_SRC; src++)
+ if (members & BIT(src))
+ order[n++] = src;
+
+ /* Insertion sort: at most a dozen entries. */
+ for (i = 1; i < n; i++) {
+ u8 cur = order[i];
+
+ for (j = i; j > 0; j--) {
+ u8 prev = order[j - 1];
+ u16 kc, kp;
+
+ kc = (chan->cfg_known & BIT(cur)) ?
+ chan->cfg_prio[cur] : U8_MAX;
+ kp = (chan->cfg_known & BIT(prev)) ?
+ chan->cfg_prio[prev] : U8_MAX;
+ if (kc == kp) {
+ /* Keep the hardware order among equals. */
+ kc = chan->prio_last[cur] ?: U8_MAX;
+ kp = chan->prio_last[prev] ?: U8_MAX;
+ }
+ if (kp <= kc)
+ break;
+ order[j] = prev;
+ }
+ order[j] = cur;
+ }
+
+ for (i = 0; i < SIT9531X_PRIO_MAX_SLOTS; i++)
+ srcs[i] = i < n ? order[i] : SIT9531X_PRIO_SRC_NONE;
+}
+
+/*
+ * Refresh a PLL's cached view of its priority table from the source codes
+ * the table holds -- here after a write, and once per poll from the
+ * read-back in sit9531x_chan_state_fetch().
+ *
+ * The membership mask is what the pin state getters test, the per-slot
+ * copy is what a rewrite compares against, and the first-slot array
+ * orders sources of equal priority, so none of them costs a register read
+ * per pin.
*/
static void sit9531x_prio_mask_build(struct sit9531x_dev *sitdev, u8 pll_idx,
const u8 *srcs)
{
+ struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+ u8 first[SIT9531X_PRIO_NUM_SRC] = { 0 };
u16 mask = 0;
- u8 slot;
+ u8 slot, src, src_canon;
for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
- u8 src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
+ src = srcs[slot];
+ src &= SIT9531X_PRIO_NIBBLE_MASK;
+ chan->prio_srcs[slot] = src;
+ src_canon = sit9531x_prio_src_canon(sitdev, src);
+ if (!sit9531x_prio_src_usable(src))
+ continue;
+
+ mask |= BIT(src_canon);
+ if (!first[src_canon])
+ first[src_canon] = slot + 1;
+ }
+
+ /*
+ * Assign unconditionally: a source that has left the table has no
+ * slot, and leaving its old one behind would keep reporting it as
+ * listed for as long as the device runs.
+ */
+ for (src = 0; src < SIT9531X_PRIO_NUM_SRC; src++)
+ chan->prio_last[src] = first[src];
+
+ chan->prio_mask = mask;
+}
+
+/* Attempts to release a forced holdover before reporting it stuck. */
+#define SIT9531X_HO_CLEAR_TRIES 3
+
+static int sit9531x_prio_table_read(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 *srcs);
- if (sit9531x_prio_src_usable(src))
- mask |= BIT(src);
+/*
+ * First source in a table that the device could lock to now, compared as
+ * canonical codes, or SIT9531X_PRIO_SRC_NONE when no listed source has a
+ * signal.
+ */
+static u8 sit9531x_prio_top_live(const struct sit9531x_dev *sitdev,
+ const u8 *srcs)
+{
+ u8 i, src;
+
+ for (i = 0; i < SIT9531X_PRIO_MAX_SLOTS; i++) {
+ src = srcs[i] & SIT9531X_PRIO_NIBBLE_MASK;
+ src = sit9531x_prio_src_canon(sitdev, src);
+ if (sit9531x_prio_src_usable(src) &&
+ sit9531x_prio_src_live(sitdev, src))
+ return src;
}
- sitdev->chan[pll_idx].prio_mask = mask;
+ return SIT9531X_PRIO_SRC_NONE;
+}
+
+/*
+ * Choose the active selection for a table about to be latched. After a
+ * table write the PLL goes to the source the selection names; it moves to
+ * another on its own only when that source loses its signal, which is an
+ * event, not a state. So the selection has to name a source the PLL can
+ * use, and it follows the priorities the way the DPLL interface defines
+ * automatic mode -- the highest-priority valid input:
+ *
+ * - When the highest-priority source with signal is not the one the
+ * table held before, the priorities now put another source first, and
+ * the selection goes to it.
+ * - Otherwise the write only reorders sources below it, or removes one
+ * the PLL is not on, and the selection stays where it is while that
+ * source is still listed and has signal: a change further down the
+ * table must not pull a PLL off a healthy reference.
+ * - Otherwise the first listed source with signal; with none alive the
+ * first listed one is as good as any.
+ *
+ * A selection that is still listed but has lost its signal is moved too.
+ * The PLL has then fallen back on its own, and this driver does not read
+ * which source; left alone, the next table write sends it back to the
+ * dead one and it unlocks. The device falls back to the best listed
+ * source that has signal, which is the one chosen here, so moving the
+ * selection there does not move the PLL.
+ *
+ * @old is the table the device holds before this write.
+ */
+static u8 sit9531x_prio_activesel_pick(struct sit9531x_dev *sitdev,
+ const u8 *old, const u8 *srcs, u8 cur)
+{
+ u8 top, i;
+
+ top = sit9531x_prio_top_live(sitdev, srcs);
+ if (top != SIT9531X_PRIO_SRC_NONE &&
+ top != sit9531x_prio_top_live(sitdev, old))
+ return top;
+
+ /*
+ * The table is built from canonical codes, so compare in the same
+ * terms: a differential pair selected through its N-lane code is the
+ * P-lane entry.
+ */
+ cur = sit9531x_prio_src_canon(sitdev, cur & SIT9531X_PRIO_NIBBLE_MASK);
+
+ if (sit9531x_prio_src_usable(cur) &&
+ sit9531x_prio_src_live(sitdev, cur))
+ for (i = 0; i < SIT9531X_PRIO_MAX_SLOTS; i++)
+ if (srcs[i] == cur)
+ return cur;
+
+ if (top != SIT9531X_PRIO_SRC_NONE)
+ return top;
+
+ return srcs[0];
+}
+
+static int sit9531x_prio_table_commit(struct sit9531x_dev *sitdev, u8 pll_idx,
+ const u8 *srcs)
+{
+ struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+ u8 val, slot, attempt, written = 0, restored = 0;
+ u8 now[SIT9531X_PRIO_MAX_SLOTS];
+ int rc = 0, prg_rc, ho_rc = 0;
+ bool empty;
+ u16 reg;
+
+ empty = !sit9531x_prio_src_usable(srcs[0]);
+
+ 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);
+
+ /*
+ * Two slots share a register, and this writes every slot, so both
+ * nibbles are known for every register but the last -- build those
+ * bytes outright. Reading first would raise the question of what a
+ * read returns between the write and the latch, and the answer does
+ * not matter if nothing is read.
+ */
+ for (slot = 0; slot + 1 < SIT9531X_PRIO_MAX_SLOTS; slot += 2) {
+ reg = sit9531x_prio_reg(pll_idx, slot);
+
+ val = sit9531x_prio_slot_set(0, slot, srcs[slot]);
+ val = sit9531x_prio_slot_set(val, slot + 1, srcs[slot + 1]);
+
+ rc = sit9531x_write_u8(sitdev, reg, val);
+ if (rc)
+ goto rollback;
+
+ written = slot + 2;
+ }
+
+ /*
+ * The last register carries slot 10 in its high nibble and the
+ * device's active selection in its low one; see
+ * sit9531x_prio_activesel_pick() for how the selection is chosen.
+ * It needs the signal state now, not as of the last poll.
+ *
+ * The slot setter picks its nibble by parity, so the selection is
+ * addressed as the slot past the last one. This register has not
+ * been written yet in this sequence, so the read returns what the
+ * device is running with.
+ *
+ * A table naming no source at all is what removing the last one
+ * asks for. There is nothing to point the selection at -- the code
+ * for no source is not one the selection takes -- so the nibble is
+ * left alone and the PLL is kept in holdover below instead.
+ */
+ reg = sit9531x_prio_reg(pll_idx, slot);
+
+ rc = sit9531x_read_u8(sitdev, reg, &val);
+ if (rc)
+ goto rollback;
+
+ val = sit9531x_prio_slot_set(val, slot, srcs[slot]);
+
+ if (!empty) {
+ u8 sel = sit9531x_prio_slot_get(val, slot + 1);
+
+ if (sit9531x_input_mon_fetch(sitdev))
+ dev_warn_ratelimited(sitdev->dev,
+ "PLL%c: input monitor not read; choosing the selection without it\n",
+ 'A' + pll_idx);
+ sel = sit9531x_prio_activesel_pick(sitdev, chan->prio_srcs,
+ srcs, sel);
+ val = sit9531x_prio_slot_set(val, slot + 1, sel);
+ }
+
+ rc = sit9531x_write_u8(sitdev, reg, val);
+ if (rc)
+ goto rollback;
+
+ written = SIT9531X_PRIO_MAX_SLOTS;
+
+rollback:
+ if (rc && written) {
+ /*
+ * Put the slots that did reach the device back the way they
+ * were. Latching a table that is neither the previous order
+ * nor the requested one hands the reference selection loop
+ * a priority list nobody asked for. The cache is the table
+ * as last read, which is what those slots held.
+ */
+ for (slot = 0; slot < written; slot += 2) {
+ u8 old;
+
+ old = sit9531x_prio_slot_set(0, slot,
+ chan->prio_srcs[slot]);
+ old = sit9531x_prio_slot_set(old, slot + 1,
+ chan->prio_srcs[slot + 1]);
+ if (sit9531x_write_u8(sitdev,
+ sit9531x_prio_reg(pll_idx, slot),
+ old))
+ break;
+
+ restored = slot + 2;
+ }
+ written = restored;
+ }
+
+ /*
+ * Latch unconditionally: the slots that reached the device are in
+ * the table regardless, so the latch keeps hardware and the cache
+ * refresh below consistent with what was actually written.
+ */
+ prg_rc = sit9531x_prio_prg_commit(sitdev);
+ if (prg_rc && !rc)
+ rc = prg_rc;
+
+ /*
+ * Refresh the cache so a get that follows a set does not have to
+ * wait for the next poll. After a complete write that is the table
+ * just written. After a failure it is whatever the device holds
+ * now -- part request, part restore -- so read it back rather than
+ * piece it together: the membership test decides what a failed
+ * request rolls back, and it must not answer for writes that did not
+ * land. A read-back that fails too leaves the next poll to do it.
+ */
+ if (!rc) {
+ sit9531x_prio_mask_build(sitdev, pll_idx, srcs);
+ memcpy(chan->seen_srcs, srcs, sizeof(chan->seen_srcs));
+ chan->seen_valid = true;
+ } else if (!sit9531x_prio_table_read(sitdev, pll_idx, now)) {
+ sit9531x_prio_mask_build(sitdev, pll_idx, now);
+ memcpy(chan->seen_srcs, now, sizeof(chan->seen_srcs));
+ chan->seen_valid = true;
+ }
+
+ /*
+ * A table that names no source keeps the PLL in the holdover forced
+ * above: that is the one state in which it follows no input, which
+ * is what disconnecting every input asks for. The selection nibble
+ * alone would not do it -- it still names the old source, and the
+ * PLL keeps following that one for as long as it has signal. The
+ * next table write that lists a source releases it.
+ */
+ if (empty && !rc) {
+ dev_dbg(sitdev->dev,
+ "PLL%c: no source listed, holdover kept\n",
+ 'A' + pll_idx);
+ return 0;
+ }
+
+ /*
+ * Release the forced holdover. Apart from an empty table, nothing
+ * in the driver keeps this bit set, so a PLL left with it reports
+ * holdover until the next table write on the same PLL clears it,
+ * which may never come. Retry before giving up, and say so if it
+ * stays set.
+ */
+ for (attempt = 0; attempt < SIT9531X_HO_CLEAR_TRIES; attempt++) {
+ ho_rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_HO_CTRL,
+ BIT(SIT9531X_PLL_HO_FORCE_BIT),
+ 0);
+ if (!ho_rc)
+ break;
+ usleep_range(1000, 2000);
+ }
+ if (ho_rc) {
+ dev_err(sitdev->dev, "PLL%c left in forced holdover: %d\n",
+ 'A' + pll_idx, ho_rc);
+ if (!rc)
+ rc = ho_rc;
+ }
+
+ return rc;
}
/*
@@ -479,6 +1083,137 @@ static int sit9531x_prio_table_read(struct sit9531x_dev *sitdev, u8 pll_idx,
return 0;
}
+/*
+ * Rewrite a PLL's table for a new member set, unless it would come out as
+ * the table already holds: every write forces the PLL into holdover for
+ * the length of the sequence, so one that changes nothing is a
+ * disturbance nobody asked for.
+ */
+static int sit9531x_prio_table_apply(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u16 members)
+{
+ u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+
+ if (hweight16(members) > SIT9531X_PRIO_MAX_SLOTS)
+ return -ENOSPC;
+
+ sit9531x_prio_table_build(sitdev, pll_idx, members, srcs);
+ if (!memcmp(srcs, sitdev->chan[pll_idx].prio_srcs, sizeof(srcs)))
+ return 0;
+
+ return sit9531x_prio_table_commit(sitdev, pll_idx, srcs);
+}
+
+/*
+ * sit9531x_input_prio_set - set an input's priority on a PLL
+ * @input_idx: input source in hardware encoding (0-11, see
+ * sit9531x_input_hw_src())
+ * @prio: priority, lower is preferred
+ *
+ * Records the priority and, when the source is in the PLL's table,
+ * rebuilds the table from the configured priorities. A source that is
+ * not in the table keeps the priority for when it is connected: that is
+ * the pin's state, and it belongs to the state setter. Other sources
+ * keep theirs either way, so no sibling's priority moves.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, -EINVAL for a bad PLL or source, <0 on error
+ */
+int sit9531x_input_prio_set(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 input_idx, u8 prio)
+{
+ struct sit9531x_chan *chan;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+ input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+ if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+ return -EINVAL;
+
+ chan = &sitdev->chan[pll_idx];
+ chan->cfg_prio[input_idx] = prio;
+ chan->cfg_known |= BIT(input_idx);
+
+ if (!(chan->prio_mask & BIT(input_idx)))
+ return 0;
+
+ return sit9531x_prio_table_apply(sitdev, pll_idx, chan->prio_mask);
+}
+
+/*
+ * sit9531x_input_prio_remove - drop an input from a PLL's priority table
+ * @input_idx: input source in hardware encoding
+ *
+ * Rebuilds the table without the source, which makes a disconnected
+ * input ineligible for automatic reference selection, not just gated at
+ * the input buffer. The source keeps its configured priority for when it
+ * comes back. Removing a source that is absent succeeds without touching
+ * the table. Removing the last one leaves a table that names no source;
+ * the commit then keeps the PLL in holdover, which is what disconnecting
+ * every input asks for.
+ *
+ * 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)
+{
+ struct sit9531x_chan *chan;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+ input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+ if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+ return -EINVAL;
+
+ chan = &sitdev->chan[pll_idx];
+ if (!(chan->prio_mask & BIT(input_idx)))
+ return 0;
+
+ return sit9531x_prio_table_apply(sitdev, pll_idx,
+ chan->prio_mask & ~BIT(input_idx));
+}
+
+/*
+ * sit9531x_input_prio_add - make an input eligible in a PLL's table
+ * @input_idx: input source in hardware encoding
+ *
+ * Puts the source back into the table at its configured priority; one the
+ * PLL never listed has the lowest slot (see sit9531x_prio_cfg_seed()). A
+ * source that is already listed leaves the table untouched.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, -ENOSPC when the table cannot hold another
+ * source, <0 on error
+ */
+int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 input_idx)
+{
+ struct sit9531x_chan *chan;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+ input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+ if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+ return -EINVAL;
+
+ chan = &sitdev->chan[pll_idx];
+ if (chan->prio_mask & BIT(input_idx))
+ return 0;
+
+ return sit9531x_prio_table_apply(sitdev, pll_idx,
+ chan->prio_mask | BIT(input_idx));
+}
+
/* XO doubler register */
#define SIT9531X_REG_XO2_GENERIC SIT9531X_REG(0x00, 0x2D)
#define SIT9531X_XO_DOUBLER_ENB_BIT 7 /* inverted: 0 = enabled */
@@ -569,7 +1304,8 @@ int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
* @index: logical input index
*
* Reads whether the lane's receiver is on, from the Page 0x02 force and
- * state bits.
+ * state bits. Signal status comes from sit9531x_input_mon_fetch(),
+ * which reads every lane's clock monitor in one pass.
*/
static int sit9531x_ref_state_fetch(struct sit9531x_dev *sitdev, u8 index)
{
@@ -739,12 +1475,27 @@ static int sit9531x_chan_state_fetch(struct sit9531x_dev *sitdev, u8 pll_idx)
sit9531x_prio_mask_build(sitdev, pll_idx, srcs);
+ /*
+ * The configured priorities come from the table the first time it
+ * is read, and again whenever it no longer matches what the driver
+ * last wrote: something else -- a profile reload, a direct I2C
+ * tool -- rewrote it, and that table is now the configuration.
+ */
+ if (!chan->seen_valid ||
+ memcmp(srcs, chan->seen_srcs, sizeof(chan->seen_srcs)))
+ sit9531x_prio_cfg_seed(sitdev, pll_idx, srcs);
+
/* STATUS_1_GENERIC reports loss of lock, so invert it. */
chan->active = active;
chan->locked = active && !(outer_lol & BIT(pll_idx));
chan->mode = !!(status & SIT9531X_PLL_STATUS_OUTER_DIS);
- chan->selected_ref =
- sit9531x_hw_src_input(input_sel & SIT9531X_PRIO_NIBBLE_MASK);
+ /*
+ * Canonicalise like the table entries: a differential pair selected
+ * through its N-lane code is the P-lane pin.
+ */
+ input_sel &= SIT9531X_PRIO_NIBBLE_MASK;
+ input_sel = sit9531x_prio_src_canon(sitdev, input_sel);
+ chan->selected_ref = sit9531x_hw_src_input(input_sel);
chan->inner_lol = !!(inner_lol & BIT(pll_idx));
chan->ho_freeze = !!(ho_freeze & BIT(pll_idx));
chan->ho_valid = !!(pll_status_1 & SIT9531X_PLL_STATUS_1_HO_VALID);
@@ -913,6 +1664,13 @@ static int sit9531x_dev_state_fetch(struct sit9531x_dev *sitdev)
}
}
+ rc = sit9531x_input_mon_fetch(sitdev);
+ if (rc) {
+ dev_err(sitdev->dev,
+ "Failed to read the input clock monitors: %d\n", 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.
@@ -961,6 +1719,46 @@ static void sit9531x_dev_ref_states_update(struct sit9531x_dev *sitdev)
dev_warn(sitdev->dev,
"Failed to get REF%u status: %d\n", i, rc);
}
+
+ rc = sit9531x_input_mon_fetch(sitdev);
+ if (rc)
+ dev_warn(sitdev->dev,
+ "Failed to read the input clock monitors: %d\n", rc);
+}
+
+/*
+ * sit9531x_ref_pll_mask_rebuild - re-derive the input receiver refcounts
+ *
+ * ref->pll_mask decides when an input receiver may be powered down, and
+ * the connect and disconnect paths maintain it by hand. A request that
+ * failed part way through leaves it describing a table the device does
+ * not hold, and nothing else corrected it: a later disconnect could then
+ * drop the count to zero and gate an input another PLL is still locked
+ * to. Re-derive every mask from the tables the poll has just read. No
+ * extra bus traffic -- sit9531x_chan_state_fetch() refreshed the masks
+ * this reads immediately before.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static void sit9531x_ref_pll_mask_rebuild(struct sit9531x_dev *sitdev)
+{
+ u8 pll_idx, src, index;
+
+ for (index = 0; index < sitdev->info->num_inputs; index++)
+ sitdev->ref[index].pll_mask = 0;
+
+ for (pll_idx = 0; pll_idx < SIT9531X_NUM_PLLS; pll_idx++) {
+ u16 mask = sitdev->chan[pll_idx].prio_mask;
+
+ for (src = 0; src < SIT9531X_PRIO_NUM_SRC; src++) {
+ if (!(mask & BIT(src)))
+ continue;
+
+ index = sit9531x_hw_src_input(src);
+ if (index < sitdev->info->num_inputs)
+ sitdev->ref[index].pll_mask |= BIT(pll_idx);
+ }
+ }
}
static void sit9531x_dev_chan_states_update(struct sit9531x_dev *sitdev)
@@ -974,6 +1772,8 @@ static void sit9531x_dev_chan_states_update(struct sit9531x_dev *sitdev)
"Failed to get PLL%c state: %d\n",
'A' + i, rc);
}
+
+ sit9531x_ref_pll_mask_rebuild(sitdev);
}
/*
@@ -1265,6 +2065,84 @@ static void sit9531x_pll_states_report(struct sit9531x_dev *sitdev)
}
}
+/*
+ * sit9531x_input_pin_is_registrable - check if an input pin is registrable
+ *
+ * Split out so input-model changes stay local to this helper.
+ *
+ * Return: true if the input pin should be registered, false otherwise
+ */
+static bool sit9531x_input_pin_is_registrable(struct sit9531x_dev *sitdev,
+ u8 index)
+{
+ if (index >= sitdev->info->num_inputs)
+ return false;
+
+ /*
+ * The N lane of a differentially-configured pair is not a
+ * standalone input and is skipped (zl3073x model).
+ */
+ if (sit9531x_input_is_n(index) &&
+ sitdev->ref[index].sig_mode == SIT9531X_MODE_DE)
+ return false;
+
+ return true;
+}
+
+/*
+ * Warn about a pin node in the firmware description whose reg names no
+ * pin this device can have: an input lane or an output the variant does
+ * not have, or the N lane of a pair the configuration runs differential.
+ * Nodes are looked up from the pins (sit9531x_pin_props_get()), so such a
+ * node would otherwise have its label and frequencies dropped without a
+ * word. The binding bounds reg per variant; this catches what reaches
+ * the driver unvalidated, and the pair mode, which only the loaded
+ * configuration decides. An output that exists but that no PLL drives
+ * gets no pin either and is not reported: which outputs a configuration
+ * uses is not a fault in the description.
+ */
+static void sit9531x_pin_nodes_check(struct sit9531x_dev *sitdev)
+{
+ struct fwnode_handle *pins, *node;
+ bool found;
+ u32 reg;
+ u8 i;
+
+ pins = device_get_named_child_node(sitdev->dev, "input-pins");
+ fwnode_for_each_child_node(pins, node) {
+ if (fwnode_property_read_u32(node, "reg", ®))
+ continue;
+ if (reg < sitdev->info->num_inputs &&
+ sit9531x_input_pin_is_registrable(sitdev, reg))
+ continue;
+ dev_warn(sitdev->dev,
+ "input-pins/%pfwP: reg %u is %s, node ignored\n",
+ node, reg,
+ reg < sitdev->info->num_inputs ?
+ "the N lane of a differential pair" :
+ "not an input lane");
+ }
+ fwnode_handle_put(pins);
+
+ pins = device_get_named_child_node(sitdev->dev, "output-pins");
+ fwnode_for_each_child_node(pins, node) {
+ if (fwnode_property_read_u32(node, "reg", ®))
+ continue;
+ found = false;
+ for (i = 0; i < sitdev->info->num_outputs; i++) {
+ if (sitdev->info->clkout_map[i] == reg) {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ dev_warn(sitdev->dev,
+ "output-pins/%pfwP: reg %u is not an output of %s, node ignored\n",
+ node, reg, sitdev->info->name);
+ }
+ fwnode_handle_put(pins);
+}
+
/*
* sit9531x_dev_start - start normal operation
*
@@ -1287,6 +2165,8 @@ int sit9531x_dev_start(struct sit9531x_dev *sitdev)
sit9531x_pll_states_report(sitdev);
mutex_unlock(&sitdev->multiop_lock);
+ sit9531x_pin_nodes_check(sitdev);
+
list_for_each_entry(sitdpll, &sitdev->dplls, list) {
rc = sit9531x_dpll_register(sitdpll);
if (rc) {
@@ -1468,9 +2348,9 @@ static void sit9531x_dpll_pins_unregister(struct sit9531x_dpll *sitdpll)
* @dir: pin direction
* @index: pin hardware index
*
- * Only the XO pin has a complete pin-op table in this patch, so only
- * the XO pin is registrable here. Other pin classes are registered
- * once their state callbacks land in the following patches.
+ * For input pins: delegate to sit9531x_input_pin_is_registrable().
+ * A pin class whose state callback the tree does not have yet is not
+ * registrable: the core refuses a pin without one.
*
* Return: true if pin should be registered, false otherwise
*/
@@ -1478,15 +2358,15 @@ static bool sit9531x_dpll_pin_is_registrable(struct sit9531x_dpll *sitdpll,
enum dpll_pin_direction dir,
u8 index)
{
- /*
- * Only the XO pin has a complete pin-op table in this patch.
- * Other pin classes are registered once their state callbacks
- * land in the following patches.
- */
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+
if (dir != DPLL_PIN_DIRECTION_INPUT)
return false;
- return index == SIT9531X_MAX_INPUTS;
+ if (index == SIT9531X_MAX_INPUTS)
+ return true;
+
+ return sit9531x_input_pin_is_registrable(sitdev, index);
}
/*
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 28fc99f50063..adcdfc46c597 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -77,6 +77,9 @@ enum sit9531x_signal_mode {
* @freq: configured frequency in Hz
* @enabled: the lane's receiver is on
* @pll_mask: bitmask of PLLs this input feeds (bit 0 = PLLA)
+ * @los: the clock monitor reports loss of signal on the lane
+ * @qual_fail: the clock monitor reports a frequency drift on the
+ * lane while it still has signal
* @sig_mode: signal mode of the pair this lane belongs to
* (detected from CLKINx_INPUT_MODE at probe)
*/
@@ -84,6 +87,8 @@ struct sit9531x_ref {
u64 freq;
bool enabled;
u8 pll_mask;
+ bool los;
+ bool qual_fail;
enum sit9531x_signal_mode sig_mode;
};
@@ -121,7 +126,13 @@ struct sit9531x_out {
* @ho_freeze: holdover freeze active
* @ho_valid: holdover memory acquired, i.e. the holdover window
* holds a valid estimate to fall back on
- * @prio_mask: bit per hardware source code present in this PLL's
+ * @prio_srcs: cached copy of the priority table, one source code
+ * per slot; refreshed together with @prio_mask, it is
+ * what a rebuilt table is compared against
+ * @prio_last: first slot each source occupies, plus one (0 = the
+ * source is not in the table); refreshed from the same
+ * scan as @prio_mask, so the two never disagree
+ * @prio_mask: bit per canonical source present in this PLL's
* priority table, i.e. the sources it may select. Read
* back from the table by the periodic worker and
* refreshed by every table write, so it tracks the
@@ -145,6 +156,8 @@ struct sit9531x_chan {
bool inner_lol;
bool ho_freeze;
bool ho_valid;
+ u8 prio_srcs[SIT9531X_PRIO_MAX_SLOTS];
+ u8 prio_last[SIT9531X_PRIO_NUM_SRC];
u16 prio_mask;
u8 cfg_prio[SIT9531X_PRIO_NUM_SRC];
u16 cfg_known;
@@ -229,8 +242,18 @@ 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 ---- */
+bool sit9531x_input_prio_present(struct sit9531x_dev *sitdev,
+ u8 pll_idx, u8 input_idx);
+int sit9531x_input_prio_set(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 input_idx, u8 prio);
+int sit9531x_input_prio_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) ---- */
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index 924386aec4d8..f961b7af28fe 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -219,6 +219,125 @@ const struct dpll_device_ops sit9531x_dpll_device_ops = {
* FIXED role -- XO pin. Always CONNECTED; it cannot be routed.
*/
+/*
+ * Report a selection-role pin's state on this DPLL. @pin_id is a logical
+ * input index, SIT9531X_INTSYNC_PIN_ID for the INTSYNC destination.
+ *
+ * Membership comes from chan->prio_mask, which is the priority table read
+ * back from the chip -- not a record of what the driver asked for. The
+ * getter runs on every poll for every input pin of every DPLL, so it takes
+ * the mask the worker refreshed rather than rescanning the table over I2C
+ * each time; table writes refresh it too, so a get right after a set does
+ * not report the old membership.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static void
+sit9531x_dpll_selection_state_get(struct sit9531x_dev *sitdev,
+ const struct sit9531x_dpll *sitdpll,
+ u8 pin_id, enum dpll_pin_state *state)
+{
+ const struct sit9531x_chan *chan;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
+
+ if (chan->prio_mask & BIT(sit9531x_input_hw_src(pin_id)))
+ *state = DPLL_PIN_STATE_SELECTABLE;
+ else
+ *state = DPLL_PIN_STATE_DISCONNECTED;
+}
+
+/*
+ * Is this the reference the PLL is tracking now? See the S && L && !N
+ * predicate in the pin-state contract. This is also what gates the
+ * measurements taken against the active reference.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static bool
+sit9531x_dpll_selection_active(struct sit9531x_dev *sitdev,
+ const struct sit9531x_dpll *sitdpll, u8 pin_id)
+{
+ const struct sit9531x_chan *chan;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
+
+ if (chan->selected_ref != pin_id || !chan->locked || chan->mode ||
+ chan->ho_freeze)
+ return false;
+
+ /*
+ * A selection naming a lane without signal is not what the PLL runs
+ * on: the device has fallen back to another listed source on its
+ * own, and this driver does not read which. Report no pin as
+ * active then, rather than the dead one.
+ */
+ if (pin_id < sitdev->info->num_inputs &&
+ sit9531x_ref_state_get(sitdev, pin_id)->los)
+ return false;
+
+ return true;
+}
+
+/*
+ * Report a selection-role pin's operational state on this DPLL.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static void
+sit9531x_dpll_selection_operstate_get(struct sit9531x_dev *sitdev,
+ const struct sit9531x_dpll *sitdpll,
+ u8 pin_id,
+ enum dpll_pin_operstate *operstate)
+{
+ const struct sit9531x_ref *ref;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (sit9531x_dpll_selection_active(sitdev, sitdpll, pin_id)) {
+ *operstate = DPLL_PIN_OPERSTATE_ACTIVE;
+ return;
+ }
+
+ if (pin_id < sitdev->info->num_inputs) {
+ ref = sit9531x_ref_state_get(sitdev, pin_id);
+ if (ref->los) {
+ *operstate = DPLL_PIN_OPERSTATE_NO_SIGNAL;
+ return;
+ }
+ if (ref->qual_fail) {
+ *operstate = DPLL_PIN_OPERSTATE_QUAL_FAILED;
+ return;
+ }
+ }
+
+ *operstate = DPLL_PIN_OPERSTATE_STANDBY;
+}
+
+static int
+sit9531x_dpll_input_pin_operstate_on_dpll_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_operstate *state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+
+ mutex_lock(&sitdev->multiop_lock);
+ sit9531x_dpll_selection_operstate_get(sitdev, sitdpll, dpin->id,
+ state);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return 0;
+}
+
static int
sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
void *pin_priv,
@@ -231,8 +350,163 @@ sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
return 0;
}
+/*
+ * sit9531x_dpll_input_pin_state_on_dpll_get - get input pin DPLL state
+ *
+ * Selection role; see the pin-state contract above.
+ */
+static int
+sit9531x_dpll_input_pin_state_on_dpll_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state *state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+
+ mutex_lock(&sitdev->multiop_lock);
+ sit9531x_dpll_selection_state_get(sitdev, sitdpll, dpin->id, state);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_input_pin_state_on_dpll_set - set input pin DPLL state
+ *
+ * Enables or disables the physical input receiver via Page 0x02
+ * force/state registers (sit9531x_input_disable/enable()) and updates
+ * this DPLL's Page 1 priority table so the state is honoured by the
+ * PLL's automatic reference selection, not just at the input buffer.
+ * Selection role; see the pin-state contract above for the states.
+ *
+ * The priority table is per PLL, so it is always updated for this DPLL.
+ * A single physical input feeds every DPLL, so the hardware receiver is
+ * only cut off once the last DPLL has released it: ref->pll_mask tracks
+ * which DPLLs currently claim the input, and the physical disable
+ * happens on the transition to an empty mask.
+ */
+static int
+sit9531x_dpll_input_pin_state_on_dpll_set(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ struct sit9531x_ref *ref = &sitdev->ref[dpin->id];
+ u8 hw_src = sit9531x_input_hw_src(dpin->id);
+ u8 pll_bit = BIT(sitdpll->id);
+ bool enabled_here = false;
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ switch (state) {
+ case DPLL_PIN_STATE_DISCONNECTED:
+ rc = sit9531x_input_prio_remove(sitdev, sitdpll->id, hw_src);
+ /*
+ * The table write, the latch and the holdover release are
+ * three steps behind one return code, so ask the table what
+ * actually happened rather than reading the errno as "no
+ * change". A source that is gone from the table has been
+ * released whatever else failed.
+ */
+ if (rc && sit9531x_input_prio_present(sitdev, sitdpll->id,
+ hw_src))
+ break;
+ ref->pll_mask &= ~pll_bit;
+ /*
+ * The receiver is shared, so the last DPLL to let go turns it
+ * off. That has to happen even when the table rewrite
+ * reported an error, or the input stays powered with nothing
+ * tracking it; the first error is the one returned.
+ */
+ if (!ref->pll_mask) {
+ int off_rc = sit9531x_input_disable(sitdev, dpin->id);
+
+ if (off_rc && !rc)
+ rc = off_rc;
+ }
+ break;
+ case DPLL_PIN_STATE_CONNECTED:
+ /*
+ * CONNECTED asks for this input and no other, which the
+ * device cannot be told to do: it selects by priority and the
+ * manual-active-select path is not wired up (see "Mode").
+ * Refuse instead of quietly behaving like SELECTABLE.
+ */
+ NL_SET_ERR_MSG(extack,
+ "Device selects its reference by priority; use selectable");
+ rc = -EOPNOTSUPP;
+ break;
+ case DPLL_PIN_STATE_SELECTABLE:
+ /*
+ * Gate the receiver on whenever it is off, not only when this
+ * DPLL holds no claim yet. The two are tracked separately --
+ * the claim comes from the priority table, the receiver from
+ * the force bits -- so a PLL that already lists the input can
+ * still find it powered down, and skipping the enable would
+ * report success for a reference that cannot reach the loop.
+ */
+ if (!ref->enabled) {
+ rc = sit9531x_input_enable(sitdev, dpin->id);
+ if (rc)
+ break;
+ enabled_here = true;
+ }
+ rc = sit9531x_input_prio_add(sitdev, sitdpll->id, hw_src);
+ if (rc && !sit9531x_input_prio_present(sitdev, sitdpll->id,
+ hw_src)) {
+ /*
+ * Undo only what this request did. A receiver the
+ * loaded configuration had already turned on is not
+ * this request's to turn off.
+ */
+ if (enabled_here)
+ sit9531x_input_disable(sitdev, dpin->id);
+ break;
+ }
+ /*
+ * Claim the input for this DPLL only once it is both enabled
+ * and present in the priority table. Setting the mask before
+ * prio_add would leak the claim if prio_add failed, keeping the
+ * shared input receiver powered even after every DPLL released
+ * it.
+ */
+ ref->pll_mask |= pll_bit;
+ break;
+ default:
+ rc = -EINVAL;
+ break;
+ }
+
+ mutex_unlock(&sitdev->multiop_lock);
+
+ /*
+ * Leave the messages the switch already set in place; only a failure
+ * that came from the hardware path still needs one.
+ */
+ if (rc == -ENOSPC)
+ NL_SET_ERR_MSG(extack,
+ "Priority table is full of unique sources on this PLL");
+ else if (rc && rc != -EOPNOTSUPP && rc != -EINVAL)
+ NL_SET_ERR_MSG(extack, "Failed to set input pin state");
+
+ return rc;
+}
+
static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
.direction_get = sit9531x_dpll_input_pin_direction_get,
+ .state_on_dpll_get = sit9531x_dpll_input_pin_state_on_dpll_get,
+ .state_on_dpll_set = sit9531x_dpll_input_pin_state_on_dpll_set,
+ .operstate_on_dpll_get = sit9531x_dpll_input_pin_operstate_on_dpll_get,
};
/*
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index 9349ec722569..d5e378715110 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -126,6 +126,10 @@
#define SIT9531X_REG_GLOBAL_UPDATE SIT9531X_REG(0x00, 0x0F)
#define SIT9531X_SMALL_UPDATE_CMD 0x02
+/* PLL holdover control (PLL page offset) */
+#define SIT9531X_PLL_REG_HO_CTRL 0x6F
+#define SIT9531X_PLL_HO_FORCE_BIT 4
+
/* One bit per input PAIR (bit 0 = CLKIN0, ..., bit 3 = CLKIN3) */
#define SIT9531X_REG_IN_DE_FORCE SIT9531X_REG(0x02, 0xE8)
#define SIT9531X_REG_IN_DE_STATE SIT9531X_REG(0x02, 0xE9)
@@ -266,6 +270,18 @@
#define SIT9531X_PLL_REG_STATUS_1 0x06
#define SIT9531X_PLL_STATUS_1_HO_VALID BIT(2)
+/*
+ * Page 0x06 clock monitor status, one nibble per input lane: P lanes in
+ * 0x02 (IN0P low, IN1P high) and 0x06 (IN2P, IN3P), N lanes in 0x92
+ * (IN0N, IN1N) and 0x96 (IN2N, IN3N).
+ */
+#define SIT9531X_CLKMON_P_STATUS_01 SIT9531X_REG(0x06, 0x02)
+#define SIT9531X_CLKMON_P_STATUS_23 SIT9531X_REG(0x06, 0x06)
+#define SIT9531X_CLKMON_N_STATUS_01 SIT9531X_REG(0x06, 0x92)
+#define SIT9531X_CLKMON_N_STATUS_23 SIT9531X_REG(0x06, 0x96)
+#define SIT9531X_CLKMON_FINE_DRIFT BIT(0)
+#define SIT9531X_CLKMON_COARSE_DRIFT BIT(1)
+#define SIT9531X_CLKMON_LOSS BIT(2)
#define SIT9531X_CLKMON_P_NOTIF_01 SIT9531X_REG(0x06, 0x03)
#define SIT9531X_CLKMON_P_NOTIF_23 SIT9531X_REG(0x06, 0x07)
--
2.43.0
^ permalink raw reply [flat|nested] 26+ messages in thread* Re: [PATCH net-next v11 07/13] dpll: sit9531x: implement input pin state on a DPLL
2026-09-30 23:37 ` [PATCH net-next v11 07/13] dpll: sit9531x: implement input pin state on a DPLL Ali Rouhi
@ 2026-10-05 1:10 ` netdev-bot+sashiko
0 siblings, 0 replies; 26+ messages in thread
From: netdev-bot+sashiko @ 2026-10-05 1:10 UTC (permalink / raw)
To: arouhi
Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba,
pabeni, robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
devicetree, netdev, linux-kernel
Thank you for your contribution! Sashiko AI review found 13 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 8 · Low: 5
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] The driver's highest-priority automatic selection only takes
effect when the table is written.
- [Medium] Even when a table write gives sit9531x_prio_activesel_pick()
the chance to move to the highest-priority healthy source, it keeps a…
- [Medium] In sit9531x_prio_table_commit(), whether forced holdover is
kept or released depends on `empty =…
- [Medium] If the table write and latch succeed but all
SIT9531X_HO_CLEAR_TRIES attempts to clear HO_FORCE fail,…
- [Medium] sit9531x_input_prio_set() writes chan->cfg_prio[input_idx] and
cfg_known before calling sit9531x_prio_table_apply(), and does not…
- [Medium] sit9531x_prio_table_commit() forces holdover on every table
write, and keeps it forced indefinitely when the table becomes empty…
- [Medium] In sit9531x_dpll_input_pin_state_on_dpll_set() SELECTABLE, a
powered-down receiver is enabled with sit9531x_input_enable() and…
- [Medium] In sit9531x_prio_table_commit(), a failed
sit9531x_input_mon_fetch() only triggers a ratelimited warning…
- [Low] sit9531x_prio_table_commit() sets the PLL's HO_CTRL force-
holdover bit (PLL page reg 0x6F, bit 4) with sit9531x_update_pll_u8()…
- [Low] Concern: this patch adds operstate_on_dpll_get for input pins,
but sit9531x_dpll_changes_check() compares only state_on_dpll_get…
- [Low] When a table commit fails and the read-back in
sit9531x_prio_table_commit() (`else if…
- [Low] sit9531x_prio_table_commit() sets HO_FORCE through
sit9531x_update_pll_u8(), a regmap_update_bits() read-modify-write,…
- [Low] The rollback in sit9531x_prio_table_commit() covers only
registers whose writes succeeded: `written = slot + 2` advances after…
--
dpll: sit9531x: implement input pin state on a DPLL
This commit adds state_on_dpll_get/set and operstate_on_dpll_get for
sit9531x input pins. SELECTABLE and DISCONNECTED map to membership in each
PLL's hardware priority table. A table rewrite forces the PLL into
holdover, writes every slot, picks the active selection from the clock
monitor state, latches, and releases holdover again.
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index bf6817b9d5656..231e2d71dd6fb 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
[ ... ]
> @@ -433,26 +526,537 @@ static u8 sit9531x_prio_slot_get(u8 val, u8 slot)
[ ... ]
> +static u8 sit9531x_prio_activesel_pick(struct sit9531x_dev *sitdev,
> + const u8 *old, const u8 *srcs, u8 cur)
> +{
> + u8 top, i;
> +
> + top = sit9531x_prio_top_live(sitdev, srcs);
> + if (top != SIT9531X_PRIO_SRC_NONE &&
> + top != sit9531x_prio_top_live(sitdev, old))
> + return top;
> +
> + /*
> + * The table is built from canonical codes, so compare in the same
> + * terms: a differential pair selected through its N-lane code is the
> + * P-lane entry.
> + */
> + cur = sit9531x_prio_src_canon(sitdev, cur & SIT9531X_PRIO_NIBBLE_MASK);
> +
> + if (sit9531x_prio_src_usable(cur) &&
> + sit9531x_prio_src_live(sitdev, cur))
> + for (i = 0; i < SIT9531X_PRIO_MAX_SLOTS; i++)
> + if (srcs[i] == cur)
> + return cur;
[Severity: Medium]
Can this keep a PLL on a lower-priority source while a higher-priority
one is healthy?
Take a table of A, B, C where A and B are live. CLK_ACTIVESEL names B
because an earlier pick ran while A had LOS. Disconnecting C builds A, B.
sit9531x_prio_top_live() returns A for both the old and the new table, so
the early return is skipped. cur is B, which is usable, live and listed,
so B is written again.
sit9531x_dpll_mode_get() always reports DPLL_MODE_AUTOMATIC. The uAPI
defines that as the highest priority input pin being auto selected by the
dpll. The commit message describes this keep-in-place policy, but it also
says the driver follows the highest-priority valid input. How do the two
fit together?
> +
> + if (top != SIT9531X_PRIO_SRC_NONE)
> + return top;
> +
> + return srcs[0];
> +}
[Severity: Medium]
Is the active selection ever looked at again once a higher-priority input
recovers?
The comment above this function says the PLL leaves the source named by
CLK_ACTIVESEL only when that source loses signal. If a table write
happens while the top-priority member has LOS, the pick chooses a
lower-priority live member.
When the preferred input comes back, sit9531x_dev_periodic_work() only
refreshes caches through sit9531x_dev_ref_states_update() and
sit9531x_dev_chan_states_update(). sit9531x_prio_activesel_pick() is
called only from sit9531x_prio_table_commit().
Doesn't that leave the PLL on the lower-priority input indefinitely,
while sit9531x_dpll_mode_get() reports DPLL_MODE_AUTOMATIC? The commit
message says the selection goes to "the highest-priority valid input,
which is how the DPLL interface defines automatic mode".
> +
> +static int sit9531x_prio_table_commit(struct sit9531x_dev *sitdev, u8 pll_idx,
> + const u8 *srcs)
> +{
[ ... ]
> + empty = !sit9531x_prio_src_usable(srcs[0]);
[Severity: Medium]
Should empty come from the table the device ends up holding, not from the
requested one?
Start with a PLL whose last input was disconnected. The empty table was
written, and the "if (empty && !rc) return 0;" exit kept HO_FORCE set.
The ACTIVESEL nibble still names the old source.
A later SELECTABLE request goes through
sit9531x_input_prio_add()->sit9531x_prio_table_apply()->
sit9531x_prio_table_commit() with a non-empty table, so empty is false.
Suppose the first slot write fails, so written stays 0. Or a later write
fails and the rollback restores the empty table. Either way the read-back
rebuilds prio_mask from a table that is still empty.
The release loop then clears HO_FORCE. Wouldn't the device be left with
an empty table, no forced holdover, and an ACTIVESEL naming the
disconnected source? The comment further down says the PLL then keeps
following that source "for as long as it has signal", while every pin
reads DISCONNECTED.
The mirror case looks similar. An empty request whose latch fails ends up
with rc != 0, so it also falls through to the release loop.
> +
> + 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;
[Severity: Low]
Can this early return leave HO_FORCE asserted?
sit9531x_update_pll_u8() is a regmap_update_bits() read-modify-write, so
an error does not prove the bit stayed clear. Later in the series,
sit9531x_output_divo_write() assumes that a write which reported an error
may still have reached the part.
Every other error path in this function goes through the release loop.
This one returns without trying to clear the bit. The callers then see
unchanged membership through sit9531x_input_prio_present() and treat the
request as having had no effect.
Should this path also try to clear the bit before returning?
> +
> + usleep_range(10000, 12000);
[ ... ]
> + for (slot = 0; slot + 1 < SIT9531X_PRIO_MAX_SLOTS; slot += 2) {
> + reg = sit9531x_prio_reg(pll_idx, slot);
> +
> + val = sit9531x_prio_slot_set(0, slot, srcs[slot]);
> + val = sit9531x_prio_slot_set(val, slot + 1, srcs[slot + 1]);
> +
> + rc = sit9531x_write_u8(sitdev, reg, val);
> + if (rc)
> + goto rollback;
> +
> + written = slot + 2;
> + }
[ ... ]
> + reg = sit9531x_prio_reg(pll_idx, slot);
> +
> + rc = sit9531x_read_u8(sitdev, reg, &val);
> + if (rc)
> + goto rollback;
> +
> + val = sit9531x_prio_slot_set(val, slot, srcs[slot]);
> +
> + if (!empty) {
> + u8 sel = sit9531x_prio_slot_get(val, slot + 1);
> +
> + if (sit9531x_input_mon_fetch(sitdev))
> + dev_warn_ratelimited(sitdev->dev,
> + "PLL%c: input monitor not read; choosing the selection without it\n",
> + 'A' + pll_idx);
> + sel = sit9531x_prio_activesel_pick(sitdev, chan->prio_srcs,
> + srcs, sel);
> + val = sit9531x_prio_slot_set(val, slot + 1, sel);
> + }
[Severity: Medium]
Is it safe to go ahead with the pick when sit9531x_input_mon_fetch()
fails?
If a register read fails, sit9531x_input_mon_fetch() returns before it
updates any ref->los. The pick then runs on values from the last poll,
which can be up to SIT9531X_STATUS_POLL_MS (500 ms) old. The comment
above says the pick "needs the signal state now, not as of the last
poll".
Say A was live at the last poll and has since lost signal. The device has
fallen back to B, but the nibble still names A. With the monitor read
failing, A still looks live, so cur = A is kept and written, and the
latch succeeds.
The comment in sit9531x_prio_activesel_pick() says such a write sends the
PLL back to the dead source and it unlocks. Wouldn't that happen here,
with the request reported as a success?
> +
> + rc = sit9531x_write_u8(sitdev, reg, val);
> + if (rc)
> + goto rollback;
> +
> + written = SIT9531X_PRIO_MAX_SLOTS;
> +
> +rollback:
> + if (rc && written) {
[ ... ]
> + for (slot = 0; slot < written; slot += 2) {
> + u8 old;
> +
> + old = sit9531x_prio_slot_set(0, slot,
> + chan->prio_srcs[slot]);
> + old = sit9531x_prio_slot_set(old, slot + 1,
> + chan->prio_srcs[slot + 1]);
> + if (sit9531x_write_u8(sitdev,
> + sit9531x_prio_reg(pll_idx, slot),
> + old))
> + break;
> +
> + restored = slot + 2;
> + }
> + written = restored;
> + }
> +
[ ... ]
> + prg_rc = sit9531x_prio_prg_commit(sitdev);
> + if (prg_rc && !rc)
> + rc = prg_rc;
[Severity: Low]
Does the rollback skip the register whose write failed?
written only advances after a successful write, and the restore loop runs
while slot < written. The register that returned the error is never
restored, even though its write may have reached the device. The latch
then runs unconditionally.
If the last register fails, written is 10. That register holds slot 10
and the CLK_ACTIVESEL nibble. Slots 0-9 are restored, but a possibly
changed slot 10 and selection are latched. sit9531x_prio_table_read()
does not read ACTIVESEL, so the read-back would not catch it.
Later in the series, sit9531x_output_divo_write() restores "the byte
whose write reported the error" (j <= written). Should this sequence do
the same?
> +
> + /*
> + * Refresh the cache so a get that follows a set does not have to
[ ... ]
> + if (!rc) {
> + sit9531x_prio_mask_build(sitdev, pll_idx, srcs);
> + memcpy(chan->seen_srcs, srcs, sizeof(chan->seen_srcs));
> + chan->seen_valid = true;
> + } else if (!sit9531x_prio_table_read(sitdev, pll_idx, now)) {
> + sit9531x_prio_mask_build(sitdev, pll_idx, now);
> + memcpy(chan->seen_srcs, now, sizeof(chan->seen_srcs));
> + chan->seen_valid = true;
> + }
[Severity: Low]
What happens to seen_srcs when the commit and this read-back both fail?
chan->seen_srcs then keeps the old table, while the device may hold a
partly written or partly restored one. On the next poll,
sit9531x_chan_state_fetch() sees the mismatch:
if (!chan->seen_valid ||
memcmp(srcs, chan->seen_srcs, sizeof(chan->seen_srcs)))
sit9531x_prio_cfg_seed(sitdev, pll_idx, srcs);
sit9531x_prio_cfg_seed() then sets cfg_prio[src] = slot for every listed
source.
Doesn't that overwrite priorities set through sit9531x_input_prio_set()
with slot positions from a table nobody asked for? The reseed check
cannot tell a failed write by the driver apart from an outside rewrite.
> +
> + /*
> + * A table that names no source keeps the PLL in the holdover forced
[ ... ]
> + if (empty && !rc) {
> + dev_dbg(sitdev->dev,
> + "PLL%c: no source listed, holdover kept\n",
> + 'A' + pll_idx);
> + return 0;
> + }
[Severity: Medium]
Should chan->ho_valid be checked before holdover is kept forced here?
For DPLL_LOCK_STATUS_HOLDOVER, the uAPI in include/uapi/linux/dpll.h says
holdover forced by disconnecting all the pins is "possible only when dpll
lock-state was already DPLL_LOCK_STATUS_LOCKED_HO_ACQ". Otherwise "the
dpll's lock-state shall remain DPLL_LOCK_STATUS_UNLOCKED".
Here, disconnecting the last input still forces holdover on a PLL that
has not acquired holdover memory. sit9531x_dpll_lock_status_get() then
reports DPLL_LOCK_STATUS_HOLDOVER from chan->ho_freeze, whatever
ho_valid says.
Doesn't that keep the device on a holdover estimate it never marked
valid, while userspace is told it is in holdover?
> +
> + /*
> + * Release the forced holdover. Apart from an empty table, nothing
[ ... ]
> + for (attempt = 0; attempt < SIT9531X_HO_CLEAR_TRIES; attempt++) {
> + ho_rc = sit9531x_update_pll_u8(sitdev, pll_idx,
> + SIT9531X_PLL_REG_HO_CTRL,
> + BIT(SIT9531X_PLL_HO_FORCE_BIT),
> + 0);
> + if (!ho_rc)
> + break;
> + usleep_range(1000, 2000);
> + }
> + if (ho_rc) {
> + dev_err(sitdev->dev, "PLL%c left in forced holdover: %d\n",
> + 'A' + pll_idx, ho_rc);
> + if (!rc)
> + rc = ho_rc;
> + }
> +
> + return rc;
> }
[Severity: Medium]
What happens if the table write and latch succeed, but all
SIT9531X_HO_CLEAR_TRIES attempts here fail?
rc was still 0 at that point, so sit9531x_prio_mask_build() has already
recorded the new membership, and the function returns ho_rc. In the
SELECTABLE handler sit9531x_input_prio_present() is now true, so the
claim is taken and the error is returned to userspace.
If the same SELECTABLE request is retried after the bus recovers,
sit9531x_input_prio_add() returns early:
if (chan->prio_mask & BIT(input_idx))
return 0;
Retrying DISCONNECTED short-circuits the same way, and so does setting an
equal priority (the memcmp() in sit9531x_prio_table_apply()).
Doesn't the retry then report success while the PLL stays in forced
holdover? The poll does not retry the release either, so the PLL seems to
stay there until some unrelated table edit runs the full sequence again.
[Severity: Low]
Is the old value of HO_FORCE meant to be thrown away?
The bit is set at the top of sit9531x_prio_table_commit() without saving
its previous value, and it is cleared here unconditionally. Say the
loaded profile or an external tool left a PLL in forced holdover. Any
SELECTABLE or DISCONNECTED change on that PLL, or a later priority set,
would then quietly release it and let it lock to a reference.
The commit message describes the sequence as "forcing the PLL into
holdover, waiting for it to take, writing every slot and releasing
holdover again". It does not say that an existing forced holdover is
lost.
[ ... ]
> @@ -479,6 +1083,137 @@ static int sit9531x_prio_table_read(struct sit9531x_dev *sitdev, u8 pll_idx,
[ ... ]
> +int sit9531x_input_prio_set(struct sit9531x_dev *sitdev, u8 pll_idx,
> + u8 input_idx, u8 prio)
> +{
[ ... ]
> + chan = &sitdev->chan[pll_idx];
> + chan->cfg_prio[input_idx] = prio;
> + chan->cfg_known |= BIT(input_idx);
> +
> + if (!(chan->prio_mask & BIT(input_idx)))
> + return 0;
> +
> + return sit9531x_prio_table_apply(sitdev, pll_idx, chan->prio_mask);
> +}
[Severity: Medium]
Should cfg_prio[] and cfg_known be restored when
sit9531x_prio_table_apply() fails?
For example, if the first HO_FORCE update in sit9531x_prio_table_commit()
fails, no table register is written, but the new priority stays recorded.
Later in the series ("dpll: sit9531x: add support to get and set priority
on input pins") this is wired to netlink. On failure,
sit9531x_dpll_input_pin_prio_set() returns the error and leaves
dpin->prio alone, but sit9531x_input_prio_get() now reports the new
value. The poll sees a changed priority and sends a notification for a
priority that was refused.
The next unrelated add or remove also rebuilds the table with the
refused priority. That can change the preferred source as a side effect.
[ ... ]
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index 924386aec4d88..f961b7af28fed 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
[ ... ]
> @@ -231,8 +350,163 @@ sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
[ ... ]
> + case DPLL_PIN_STATE_SELECTABLE:
[ ... ]
> + if (!ref->enabled) {
> + rc = sit9531x_input_enable(sitdev, dpin->id);
> + if (rc)
> + break;
> + enabled_here = true;
> + }
> + rc = sit9531x_input_prio_add(sitdev, sitdpll->id, hw_src);
[Severity: Medium]
Has the clock monitor had time to qualify a newly enabled input by the
time the selection is picked?
When the receiver was off, sit9531x_input_enable() powers it up and
sit9531x_input_prio_add() runs right away. The commit only samples the
monitors with sit9531x_input_mon_fetch() after the 10-12 ms holdover
sleep, and sit9531x_prio_activesel_pick() decides from ref->los.
A low-rate reference such as 1 PPS cannot be qualified in that window. So
the lane still shows LOSS, and the pick skips it even if it is now the
highest-priority member.
The device only leaves its selection on LOS, and no poll path rewrites
CLK_ACTIVESEL. Wouldn't the PLL stay on the lower-priority source after
the new input qualifies?
[ ... ]
> static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
> .direction_get = sit9531x_dpll_input_pin_direction_get,
> + .state_on_dpll_get = sit9531x_dpll_input_pin_state_on_dpll_get,
> + .state_on_dpll_set = sit9531x_dpll_input_pin_state_on_dpll_set,
> + .operstate_on_dpll_get = sit9531x_dpll_input_pin_operstate_on_dpll_get,
> };
[Severity: Low]
Now that operstate_on_dpll_get is added, does anything send a
dpll_pin_change_ntf() when the operstate changes?
At this commit sit9531x_dpll_changes_check() compares only the pin state:
changed = pin->seen && state != pin->pin_state;
So LOS, qual-fail and active-reference changes are not notified.
The next patch in the series, "dpll: sit9531x: add support to get and
set priority on input pins", adds the operstate comparison to
sit9531x_dpll_changes_check(). The gap seems to exist only at this
commit.
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260930233714.87679-1-arouhi%40sitime.com
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH net-next v11 08/13] dpll: sit9531x: add support to get and set priority on input pins
2026-09-30 23:37 [PATCH net-next v11 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (6 preceding siblings ...)
2026-09-30 23:37 ` [PATCH net-next v11 07/13] dpll: sit9531x: implement input pin state on a DPLL Ali Rouhi
@ 2026-09-30 23:37 ` Ali Rouhi
2026-10-05 1:10 ` netdev-bot+sashiko
2026-09-30 23:37 ` [PATCH net-next v11 10/13] dpll: sit9531x: implement output pin state on a DPLL Ali Rouhi
` (4 subsequent siblings)
12 siblings, 1 reply; 26+ messages in thread
From: Ali Rouhi @ 2026-09-30 23:37 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi, devicetree,
netdev, linux-kernel
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
When the device has to choose a reference, it takes the first slot of
its priority table whose signal is qualified. The driver keeps a
priority per input and PLL and builds the table from it (previous
patch), so getting a priority reads that value without touching a
register, and setting one records it and, for an input the PLL lists,
rewrites the table in the new order. When the new order puts a
different input with signal first, the PLL moves to it; a change further
down the table leaves the PLL where it is.
Priority and state are separate attributes, and neither moves the other.
A priority can be set on a disconnected input and takes effect when it is
connected; disconnecting an input and connecting it again brings back the
priority it had. Setting one input's priority leaves every other input's
as it was, so only the named pin changes and the core's own notification
covers it. The range is 0 to 255 against a table of eleven slots:
inputs of equal priority keep the order the table already has them in,
so distinct values are what fix an order. An input the PLL has never
listed reports the lowest slot, the priority it would be connected at.
A table rewritten behind the driver -- a profile reload, a direct I2C
tool -- is taken as the new configuration and the priorities are
re-seeded from it, so the poll now watches priority and operational state
as well as state, and notifies a pin when any of them moves. The
notifications go out after the walk over the pins, outside multiop_lock,
since the helper takes locks the callbacks run under.
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
drivers/dpll/sit9531x/core.c | 38 +++++++++
drivers/dpll/sit9531x/core.h | 2 +
drivers/dpll/sit9531x/dpll.c | 152 ++++++++++++++++++++++++++++++-----
drivers/dpll/sit9531x/dpll.h | 2 +
drivers/dpll/sit9531x/prop.c | 4 +-
5 files changed, 176 insertions(+), 22 deletions(-)
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 231e2d71dd6f..f6c76cc78456 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -670,6 +670,44 @@ bool sit9531x_input_prio_present(struct sit9531x_dev *sitdev, u8 pll_idx,
return !!(sitdev->chan[pll_idx].prio_mask & BIT(input_idx));
}
+/*
+ * sit9531x_input_prio_get - read an input's priority for a PLL
+ * @input_idx: input source in hardware encoding (see
+ * sit9531x_input_hw_src())
+ * @prio: output priority (lower is preferred)
+ *
+ * Reports the priority configured for the source on this PLL, which is
+ * kept whether or not the source is currently in the table: state and
+ * priority are separate attributes, so disconnecting an input and
+ * connecting it again must not change the priority it reports. The
+ * value is seeded from the hardware table, and re-seeded whenever the
+ * read-back shows the table was rewritten by something other than this
+ * driver. A source that was never listed reports the lowest slot.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_input_prio_get(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 input_idx, u8 *prio)
+{
+ const struct sit9531x_chan *chan;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+ input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+ if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+ return -EINVAL;
+
+ chan = &sitdev->chan[pll_idx];
+ if (chan->cfg_known & BIT(input_idx))
+ *prio = chan->cfg_prio[input_idx];
+ else
+ *prio = SIT9531X_PRIO_MAX_SLOTS - 1;
+
+ return 0;
+}
+
/*
* Take the configured priorities from a table the hardware holds: each
* listed source gets the first slot it occupies. A source the table does
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index adcdfc46c597..8aa505f84e87 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -250,6 +250,8 @@ bool sit9531x_input_prio_present(struct sit9531x_dev *sitdev,
u8 pll_idx, u8 input_idx);
int sit9531x_input_prio_set(struct sit9531x_dev *sitdev, u8 pll_idx,
u8 input_idx, u8 prio);
+int sit9531x_input_prio_get(struct sit9531x_dev *sitdev, u8 pll_idx,
+ 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,
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index f961b7af28fe..ebbf7f5a3394 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -502,11 +502,93 @@ sit9531x_dpll_input_pin_state_on_dpll_set(const struct dpll_pin *pin,
return rc;
}
+/*
+ * sit9531x_dpll_input_pin_prio_get - read input pin priority
+ *
+ * Reports the priority sit9531x_input_prio_get() keeps for the source on
+ * this PLL, connected or not; no register is read.
+ */
+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 is not touched here: it is the poll's baseline for
+ * spotting a change to notify, and a get refreshing it would hide
+ * the change from the poll.
+ */
+ *prio = slot;
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_input_pin_prio_set - set input pin priority
+ *
+ * Records the priority and, for a pin in this PLL's table, rewrites the
+ * Page 1 table in priority order (sit9531x_input_prio_set()). The other
+ * pins keep their priorities, so only the named pin changes and the core
+ * notifies it. A pin that is not in the table keeps the priority for when
+ * it is connected.
+ */
+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 > U8_MAX) {
+ NL_SET_ERR_MSG(extack, "Priority out of range (0-255)");
+ return -EINVAL;
+ }
+
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_input_prio_set(sitdev, sitdpll->id,
+ sit9531x_input_hw_src(dpin->id),
+ (u8)prio);
+ if (!rc)
+ dpin->prio = prio;
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc) {
+ NL_SET_ERR_MSG(extack, "Failed to set input priority");
+ return rc;
+ }
+
+ return 0;
+}
+
static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
.direction_get = sit9531x_dpll_input_pin_direction_get,
.state_on_dpll_get = sit9531x_dpll_input_pin_state_on_dpll_get,
.state_on_dpll_set = sit9531x_dpll_input_pin_state_on_dpll_set,
.operstate_on_dpll_get = sit9531x_dpll_input_pin_operstate_on_dpll_get,
+ .prio_get = sit9531x_dpll_input_pin_prio_get,
+ .prio_set = sit9531x_dpll_input_pin_prio_set,
};
/*
@@ -625,39 +707,67 @@ void sit9531x_dpll_changes_check(struct sit9531x_dpll *sitdpll)
dpll_device_change_ntf(sitdpll->dpll_dev);
}
+ mutex_lock(&sitdev->multiop_lock);
list_for_each_entry(pin, &sitdpll->pins, list) {
- const struct dpll_pin_ops *ops;
+ enum dpll_pin_operstate operstate;
enum dpll_pin_state state;
bool changed;
+ u8 id, prio;
/*
- * 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 state_on_dpll_get resolves to the right getter.
+ * Watch the selection-role pins -- regular references and the
+ * INTSYNC destination -- whose state, operational state and
+ * priority can move without a request: the device selects on
+ * its own, the monitors follow the signal, and a table
+ * rewritten behind the driver re-seeds the priorities.
+ * Outputs (incl. the INTSYNC source) change only through their
+ * own set callback and the XO is permanently connected.
*/
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;
+ id = pin->id;
+ if (id == SIT9531X_INTSYNC_PIN_ID &&
+ sitdev->intsync_src == sitdpll->id)
+ state = DPLL_PIN_STATE_DISCONNECTED;
+ else
+ sit9531x_dpll_selection_state_get(sitdev, sitdpll, id,
+ &state);
+ sit9531x_dpll_selection_operstate_get(sitdev, sitdpll, id,
+ &operstate);
+ if (sit9531x_input_prio_get(sitdev, sitdpll->id,
+ sit9531x_input_hw_src(id), &prio))
+ prio = pin->prio;
+
+ changed = pin->seen &&
+ (state != pin->pin_state ||
+ operstate != pin->operstate || prio != pin->prio);
+ if (changed)
+ dev_dbg(sitdev->dev,
+ "%s: state %u->%u operstate %u->%u prio %u->%u\n",
+ pin->label, pin->pin_state, state,
+ pin->operstate, operstate, pin->prio, prio);
- /*
- * The first pass only takes the baseline: the pin was
- * registered with this state, so nothing has changed yet.
- */
- changed = pin->seen && state != pin->pin_state;
pin->pin_state = state;
+ pin->operstate = operstate;
+ pin->prio = prio;
pin->seen = true;
- if (changed) {
- dev_dbg(sitdev->dev, "%s state changed\n", pin->label);
- dpll_pin_change_ntf(pin->dpll_pin);
- }
+
+ /*
+ * The notification helper takes DPLL-subsystem locks that the
+ * callbacks run under, so it cannot be called with
+ * multiop_lock held; mark the pin and send after the walk.
+ */
+ if (changed)
+ pin->ntf_pending = true;
+ }
+ mutex_unlock(&sitdev->multiop_lock);
+
+ list_for_each_entry(pin, &sitdpll->pins, list) {
+ if (!pin->ntf_pending)
+ continue;
+ pin->ntf_pending = false;
+ dpll_pin_change_ntf(pin->dpll_pin);
}
}
diff --git a/drivers/dpll/sit9531x/dpll.h b/drivers/dpll/sit9531x/dpll.h
index 1d320e183eb6..2e3df7608490 100644
--- a/drivers/dpll/sit9531x/dpll.h
+++ b/drivers/dpll/sit9531x/dpll.h
@@ -31,7 +31,9 @@ struct sit9531x_dpll_pin {
u8 id; /* hardware index */
u8 prio;
enum dpll_pin_state pin_state;
+ enum dpll_pin_operstate operstate;
bool seen; /* baseline taken by the poll */
+ bool ntf_pending;
};
/* Per-PLL DPLL device state. */
diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
index 3635ea497e41..934db566c002 100644
--- a/drivers/dpll/sit9531x/prop.c
+++ b/drivers/dpll/sit9531x/prop.c
@@ -187,10 +187,11 @@ sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
index == SIT9531X_INTSYNC_PIN_ID) {
/*
* INTSYNC destination pin: a PLL locks to the INTSYNC net as a
- * reference, so it can be connected.
+ * 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 &&
@@ -214,6 +215,7 @@ sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
} 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 {
--
2.43.0
^ permalink raw reply [flat|nested] 26+ messages in thread* Re: [PATCH net-next v11 08/13] dpll: sit9531x: add support to get and set priority on input pins
2026-09-30 23:37 ` [PATCH net-next v11 08/13] dpll: sit9531x: add support to get and set priority on input pins Ali Rouhi
@ 2026-10-05 1:10 ` netdev-bot+sashiko
0 siblings, 0 replies; 26+ messages in thread
From: netdev-bot+sashiko @ 2026-10-05 1:10 UTC (permalink / raw)
To: arouhi
Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba,
pabeni, robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
devicetree, netdev, linux-kernel
Thank you for your contribution! Sashiko AI review found 4 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 3 · Low: 1
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] sit9531x_input_prio_set() (core.c:1175-1181) stores the
requested priority with `chan->cfg_prio[input_idx] = prio;…
- [Medium] The first monitoring pass can silently absorb a real
operational-state or priority change on an input pin.
- [Medium] sit9531x_prio_src_live() (core.c:614-622) checks only
`!sitdev->ref[index].los`.
- [Low] The kernel-doc of sit9531x_dpll_input_pin_prio_set()
(dpll.c:542-546) says "The other pins keep their priorities, so only…
--
dpll: sit9531x: add support to get and set priority on input pins
This patch adds prio_get and prio_set callbacks for the sit9531x input
pins. Each input keeps a priority per PLL, and the Page 1 priority table
is rebuilt from those priorities. The poll now watches priority and
operational state as well as state, and sends pin notifications after
multiop_lock is dropped.
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index f961b7af28fed..ebbf7f5a3394e 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
[ ... ]
> +/*
> + * sit9531x_dpll_input_pin_prio_set - set input pin priority
> + *
> + * Records the priority and, for a pin in this PLL's table, rewrites the
> + * Page 1 table in priority order (sit9531x_input_prio_set()). The other
> + * pins keep their priorities, so only the named pin changes and the core
> + * notifies it. A pin that is not in the table keeps the priority for when
> + * it is connected.
> + */
[Severity: Low]
Is the claim that "only the named pin changes" accurate when the reorder
moves the active reference? The commit message says the same thing:
Setting one input's priority leaves every other input's
as it was, so only the named pin changes and the core's own notification
covers it.
sit9531x_prio_table_commit() calls sit9531x_prio_activesel_pick(). When
the new order puts another live source first, that function writes the
other source into the selection nibble:
if (top != SIT9531X_PRIO_SRC_NONE &&
top != sit9531x_prio_top_live(sitdev, old))
return top;
When that happens, the previously active pin's operstate goes from ACTIVE
to STANDBY, and the new top pin's goes the other way.
chan->selected_ref is only refreshed by sit9531x_chan_state_fetch() in the
poll. As a result, the notification that dpll_pin_prio_set() sends right
away for the named pin carries the old operstate. The sibling pins, and
the corrected operstate, are only notified by the next poll, up to
SIT9531X_STATUS_POLL_MS later.
> +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)
> +{
[ ... ]
> + mutex_lock(&sitdev->multiop_lock);
> + rc = sit9531x_input_prio_set(sitdev, sitdpll->id,
> + sit9531x_input_hw_src(dpin->id),
> + (u8)prio);
> + if (!rc)
> + dpin->prio = prio;
> + mutex_unlock(&sitdev->multiop_lock);
[Severity: Medium]
If the table write fails, does the driver keep the rejected priority
anyway? sit9531x_input_prio_set() commits the new value before calling
the step that can fail:
drivers/dpll/sit9531x/core.c:sit9531x_input_prio_set() {
...
chan->cfg_prio[input_idx] = prio;
chan->cfg_known |= BIT(input_idx);
if (!(chan->prio_mask & BIT(input_idx)))
return 0;
return sit9531x_prio_table_apply(sitdev, pll_idx, chan->prio_mask);
}
Nothing restores cfg_prio if sit9531x_prio_table_commit() fails. It can
fail on the HO_FORCE update, a slot write, the latch, or the holdover
release.
On the failure path, sit9531x_prio_table_commit() reads the table back
and refreshes seen_srcs from it:
} else if (!sit9531x_prio_table_read(sitdev, pll_idx, now)) {
sit9531x_prio_mask_build(sitdev, pll_idx, now);
memcpy(chan->seen_srcs, now, sizeof(chan->seen_srcs));
chan->seen_valid = true;
}
This means the check in sit9531x_chan_state_fetch() never re-seeds from
the hardware:
if (!chan->seen_valid ||
memcmp(srcs, chan->seen_srcs, sizeof(chan->seen_srcs)))
sit9531x_prio_cfg_seed(sitdev, pll_idx, srcs);
Meanwhile dpin->prio keeps the old value, and netlink gets an error. Can
this lead to the following?
- prio_get reports a priority that the device rejected.
- The next poll sees cfg_prio != pin->prio and sends
dpll_pin_change_ntf() for a priority the device never took.
- The next rebuild for any other reason, for example a state change on
a sibling input of the same PLL, sorts by the stale cfg_prio in
sit9531x_prio_table_build() and writes the rejected order.
sit9531x_prio_activesel_pick() could then move the PLL to a different
reference long after the failed request.
This code is unchanged at the end of the series.
[Severity: Medium]
With prio_set now available to userspace, can a reorder steer the PLL
onto an input whose monitor has disqualified it? sit9531x_prio_src_live()
only looks at LOS:
drivers/dpll/sit9531x/core.c:sit9531x_prio_src_live() {
...
return !sitdev->ref[index].los;
}
It ignores ref->qual_fail. sit9531x_input_mon_fetch() sets qual_fail on
fine or coarse frequency drift, and
sit9531x_dpll_selection_operstate_get() reports that same condition as
DPLL_PIN_OPERSTATE_QUAL_FAILED.
Suppose prio_set moves a qualification-failed input ahead of a healthy
one. sit9531x_prio_table_commit() then does this:
sel = sit9531x_prio_activesel_pick(sitdev, chan->prio_srcs,
srcs, sel);
val = sit9531x_prio_slot_set(val, slot + 1, sel);
The drifting input counts as the top live source, so it is written into
the active-selection nibble. The comment on activesel_pick says the PLL
leaves the named source only when that source loses its signal. That
seems to contradict the commit message, which says "it takes the first
slot of its priority table whose signal is qualified".
The helper comes from the previous patch in the series, but this patch
exposes the reorder that triggers it. It is unchanged at the end of the
series.
[ ... ]
> @@ -625,39 +707,67 @@ void sit9531x_dpll_changes_check(struct sit9531x_dpll *sitdpll)
[ ... ]
> + if (sit9531x_input_prio_get(sitdev, sitdpll->id,
> + sit9531x_input_hw_src(id), &prio))
> + prio = pin->prio;
> +
> + changed = pin->seen &&
> + (state != pin->pin_state ||
> + operstate != pin->operstate || prio != pin->prio);
[Severity: Medium]
Can the first poll pass silently absorb a real operstate or priority
change? The probe ordering looks like this:
sit9531x_dev_start()
sit9531x_dev_state_fetch() <- fills ref[]/chan[] caches
register DPLLs and pins <- create ntfs and GETs use the caches
kthread_queue_delayed_work(sitdev->kworker, &sitdev->work, 0);
sit9531x_dev_periodic_work()
fetch state again
sit9531x_dpll_changes_check() <- every pin still has seen == false
The pins come from kzalloc_obj() in sit9531x_dpll_pin_alloc(), so
pin->seen is false on this first pass. Whatever differs, changed is false,
and the new values become the baseline.
An input might lose signal between the initial fetch and the first tick,
or the table might be rewritten and re-seeded in that window. Userspace
would then keep the snapshot from registration, with no notification
until some other change happens.
This patch adds operstate and prio to the comparison, so those attributes
now share the gap. The cache is already valid at registration, so could
the baseline be taken there instead?
> + if (changed)
> + dev_dbg(sitdev->dev,
> + "%s: state %u->%u operstate %u->%u prio %u->%u\n",
> + pin->label, pin->pin_state, state,
> + pin->operstate, operstate, pin->prio, prio);
>
> - /*
> - * The first pass only takes the baseline: the pin was
> - * registered with this state, so nothing has changed yet.
> - */
> - changed = pin->seen && state != pin->pin_state;
> pin->pin_state = state;
> + pin->operstate = operstate;
> + pin->prio = prio;
> pin->seen = true;
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260930233714.87679-1-arouhi%40sitime.com
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH net-next v11 10/13] dpll: sit9531x: implement output pin state on a DPLL
2026-09-30 23:37 [PATCH net-next v11 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (7 preceding siblings ...)
2026-09-30 23:37 ` [PATCH net-next v11 08/13] dpll: sit9531x: add support to get and set priority on input pins Ali Rouhi
@ 2026-09-30 23:37 ` Ali Rouhi
2026-10-05 1:10 ` netdev-bot+sashiko
2026-09-30 23:37 ` [PATCH net-next v11 09/13] dpll: sit9531x: add support to get and set frequency on pins Ali Rouhi
` (3 subsequent siblings)
12 siblings, 1 reply; 26+ messages in thread
From: Ali Rouhi @ 2026-09-30 23:37 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi, devicetree,
netdev, linux-kernel
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
An output is driven by its PLL rather than selected by it, so its state
says whether it carries a signal: connected while it drives, disconnected
while it is muted. Setting the state mutes or un-mutes it by forcing the
pad to Hi-Z, the only per-output control the device offers that leaves the
divider alone.
The force bit and the state bit are separate, and a pad follows the loaded
configuration while the force bit is clear, so both are read to decide
whether an output is muted and both are written to change it. Which of
the four register banks applies depends on the slot and on whether the pad
is single-ended or differential.
This is also where output pins start being registered at all: until now
the registration filter took inputs only, so the output operations the
earlier patches added -- direction and frequency -- had nothing to
dispatch on.
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
drivers/dpll/sit9531x/core.c | 271 ++++++++++++++++++++++++++++++++++-
drivers/dpll/sit9531x/core.h | 7 +
drivers/dpll/sit9531x/dpll.c | 102 +++++++++++++
3 files changed, 374 insertions(+), 6 deletions(-)
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 3254d119b939..8fed14d8dca6 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -488,6 +488,47 @@ static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev,
muted);
}
+/*
+ * sit9531x_output_state_refresh - read an output's mute state back
+ *
+ * Used when a mute could not be confirmed at the time it was written. The
+ * driver does not poll output state, so without this the cached value would
+ * stand until something else happened to write it.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_output_state_refresh(struct sit9531x_dev *sitdev, u8 out_idx)
+{
+ bool muted;
+ int rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ rc = sit9531x_output_forced_hiz(sitdev, out_idx, &muted);
+ if (rc)
+ return rc;
+
+ sitdev->out[out_idx].enabled = !muted;
+ sitdev->out[out_idx].state_stale = false;
+
+ return 0;
+}
+
+static int sit9531x_hiz_set_bit(struct sit9531x_dev *sitdev,
+ unsigned int reg, u8 bit, bool set)
+{
+ u8 cur, new_val;
+ int rc;
+
+ rc = sit9531x_read_u8(sitdev, reg, &cur);
+ if (rc)
+ return rc;
+
+ new_val = set ? (cur | BIT(bit)) : (cur & ~BIT(bit));
+
+ return sit9531x_write_u8(sitdev, reg, new_val);
+}
+
/* Attempts to re-lock the output loops before reporting them open. */
#define SIT9531X_LOOP_LOCK_TRIES 3
@@ -621,6 +662,212 @@ static int sit9531x_prg_commit(struct sit9531x_dev *sitdev)
return 0;
}
+/*
+ * sit9531x_output_hiz_write - mute or unmute an output
+ *
+ * Muting takes control of the pin (MASK=1) and forces it to Hi-Z
+ * (STATE=0) on every Hi-Z pair of the slot -- differential, CMOS OutP
+ * and CMOS OutN -- because the output must go quiet whichever way it is
+ * wired; unmuting releases the override on all of them and hands the pin
+ * back to the device. The caller must already be in the programming
+ * state.
+ */
+static int sit9531x_output_hiz_write(struct sit9531x_dev *sitdev, u8 slot,
+ bool mute)
+{
+ struct sit9531x_hiz_pair p[SIT9531X_HIZ_PAIRS];
+ u8 mask[SIT9531X_HIZ_PAIRS], state[SIT9531X_HIZ_PAIRS];
+ int i, rc, undo_rc;
+
+ sit9531x_output_get_hiz_regs(slot, p);
+
+ if (!mute) {
+ for (i = 0; i < SIT9531X_HIZ_PAIRS; i++) {
+ rc = sit9531x_hiz_set_bit(sitdev, p[i].mask, p[i].bit,
+ false);
+ if (rc)
+ return rc;
+ }
+ return 0;
+ }
+
+ /*
+ * Remember every pair as it was: if the mute fails part-way, that
+ * is what the pad goes back to -- which may already be a mute, from
+ * an earlier request or from the loaded profile.
+ */
+ for (i = 0; i < SIT9531X_HIZ_PAIRS; i++) {
+ rc = sit9531x_read_u8(sitdev, p[i].state, &state[i]);
+ if (rc)
+ return rc;
+ rc = sit9531x_read_u8(sitdev, p[i].mask, &mask[i]);
+ if (rc)
+ return rc;
+ }
+
+ /*
+ * Forced value first, override enable second. Muted is decoded as
+ * MASK set with STATE clear, so enabling the override while STATE
+ * still holds whatever the loaded configuration left there can pin
+ * the pad driven for the width of an I2C transfer.
+ */
+ for (i = 0; i < SIT9531X_HIZ_PAIRS; i++) {
+ rc = sit9531x_hiz_set_bit(sitdev, p[i].state, p[i].bit, false);
+ if (!rc)
+ rc = sit9531x_hiz_set_bit(sitdev, p[i].mask, p[i].bit,
+ true);
+ if (rc)
+ break;
+ }
+ if (!rc)
+ return 0;
+
+ /*
+ * Only part of the mute reached the device. Put every pair it may
+ * have touched back to the bits read above -- override first, then
+ * value, the reverse of the order they went on -- so the pad ends
+ * where the request started rather than on part of a mute, or
+ * un-muted when it was muted before.
+ */
+ for (; i >= 0; i--) {
+ undo_rc = sit9531x_hiz_set_bit(sitdev, p[i].mask, p[i].bit,
+ mask[i] & BIT(p[i].bit));
+ if (!undo_rc)
+ undo_rc = sit9531x_hiz_set_bit(sitdev, p[i].state,
+ p[i].bit,
+ state[i] & BIT(p[i].bit));
+ if (undo_rc)
+ dev_err(sitdev->dev,
+ "slot%u: Hi-Z override left part applied (%d)\n",
+ slot, undo_rc);
+ }
+
+ return rc;
+}
+
+/*
+ * sit9531x_output_disable - mute an output (force Hi-Z)
+ * @index: logical output index (0..info->num_outputs-1)
+ *
+ * Sets MASK and clears STATE on every Hi-Z pair of the slot, differential
+ * and both CMOS pads, so that the output is muted regardless of its
+ * electrical configuration. The
+ * writes are wrapped in the PRG_CMD / NVM update / loop lock sequence
+ * so the new state is applied by the hardware.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_output_disable(struct sit9531x_dev *sitdev, u8 index)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ bool muted;
+ u8 slot;
+ int rc, ret, state_rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (index >= info->num_outputs)
+ return -EINVAL;
+
+ slot = info->clkout_map[index];
+ rc = sit9531x_prg_enter(sitdev);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_output_hiz_write(sitdev, slot, true);
+
+ /*
+ * Always leave the PRG_CMD programming state, even on a mid-sequence
+ * write failure: prg_enter() unlocked the output loops, so returning
+ * without prg_commit() would strand the chip in the programming state
+ * with the loops unlocked. Best effort -- keep the first error.
+ */
+ ret = sit9531x_prg_commit(sitdev);
+ if (ret && !rc)
+ rc = ret;
+
+ /*
+ * Keep the software state aligned to what hardware now drives even
+ * when one write in the sequence failed. The commit above may have
+ * applied a partial mask/state combination.
+ */
+ state_rc = sit9531x_output_forced_hiz(sitdev, index, &muted);
+ if (!state_rc) {
+ sitdev->out[index].enabled = !muted;
+ sitdev->out[index].state_stale = false;
+ } else {
+ /*
+ * The writes may well have landed; what failed is the proof.
+ * Mark the cached state for a read-through rather than
+ * reporting the value it had before this call.
+ */
+ sitdev->out[index].state_stale = true;
+ if (!rc)
+ rc = state_rc;
+ }
+
+ return rc;
+}
+
+/*
+ * sit9531x_output_enable - un-mute an output (active state)
+ * @index: logical output index (0..info->num_outputs-1)
+ *
+ * Releases MASK on every Hi-Z pair so the device drives the output
+ * again as the loaded profile configures it. The writes are wrapped
+ * in the PRG_CMD / NVM update / loop lock sequence so the new state is
+ * applied by the hardware.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_output_enable(struct sit9531x_dev *sitdev, u8 index)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ bool muted;
+ u8 slot;
+ int rc, ret, state_rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (index >= info->num_outputs)
+ return -EINVAL;
+
+ slot = info->clkout_map[index];
+ rc = sit9531x_prg_enter(sitdev);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_output_hiz_write(sitdev, slot, false);
+
+ /*
+ * Always leave the PRG_CMD programming state, even on a mid-sequence
+ * write failure: prg_enter() unlocked the output loops, so returning
+ * without prg_commit() would strand the chip in the programming state
+ * with the loops unlocked. Best effort -- keep the first error.
+ */
+ ret = sit9531x_prg_commit(sitdev);
+ if (ret && !rc)
+ rc = ret;
+
+ /* See sit9531x_output_disable(): commit can apply part of it. */
+ state_rc = sit9531x_output_forced_hiz(sitdev, index, &muted);
+ if (!state_rc) {
+ sitdev->out[index].enabled = !muted;
+ sitdev->out[index].state_stale = false;
+ } else {
+ /*
+ * The writes may well have landed; what failed is the proof.
+ * Mark the cached state for a read-through rather than
+ * reporting the value it had before this call.
+ */
+ sitdev->out[index].state_stale = true;
+ if (!rc)
+ rc = state_rc;
+ }
+
+ return rc;
+}
+
/*
* Input priority selection
*
@@ -2348,6 +2595,8 @@ static int sit9531x_out_state_fetch(struct sit9531x_dev *sitdev, u8 index)
if (rc)
return rc;
+ sitdev->out[index].state_stale = false;
+
/*
* Each PLL page holds the PLL's output-enable mask, twelve bits:
* bits 0-7 in OUT_MAP_LO and bits 8-11 in OUT_MAP_HI[3:0]. PLLA and
@@ -3165,8 +3414,8 @@ static void sit9531x_dpll_pins_unregister(struct sit9531x_dpll *sitdpll)
* @index: pin hardware index
*
* For input pins: delegate to sit9531x_input_pin_is_registrable().
- * A pin class whose state callback the tree does not have yet is not
- * registrable: the core refuses a pin without one.
+ * 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
*/
@@ -3176,13 +3425,23 @@ static bool sit9531x_dpll_pin_is_registrable(struct sit9531x_dpll *sitdpll,
{
struct sit9531x_dev *sitdev = sitdpll->dev;
- if (dir != DPLL_PIN_DIRECTION_INPUT)
+ if (dir == DPLL_PIN_DIRECTION_INPUT) {
+ if (index == SIT9531X_MAX_INPUTS)
+ return true;
+ if (index == SIT9531X_INTSYNC_PIN_ID)
+ return false;
+
+ return sit9531x_input_pin_is_registrable(sitdev, index);
+ }
+
+ if (index == SIT9531X_INTSYNC_OUT_PIN_ID)
return false;
- if (index == SIT9531X_MAX_INPUTS)
- return true;
+ if (index >= sitdev->info->num_outputs)
+ return false;
- return sit9531x_input_pin_is_registrable(sitdev, index);
+ return sitdev->out[index].pll_idx == sitdpll->id &&
+ sitdev->out[index].routed;
}
/*
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 473171a19d94..8e1378d04b77 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -99,6 +99,8 @@ struct sit9531x_ref {
* @cmos: output is wired single-ended; the Hi-Z pairs that
* speak for it are the two CMOS pad ones, not the
* differential
+ * @state_stale: the cached mute state could not be confirmed against
+ * hardware and has to be read back before it is reported
* @routed: output is mapped to @pll_idx by the initial
* configuration; an unrouted output has no DPLL pin
* @pll_idx: PLL driving this output (0-3)
@@ -107,6 +109,7 @@ struct sit9531x_out {
u64 freq;
bool enabled;
bool cmos;
+ bool state_stale;
bool routed;
u8 pll_idx;
};
@@ -258,6 +261,10 @@ 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);
+int sit9531x_output_state_refresh(struct sit9531x_dev *sitdev,
+ u8 out_idx);
/* ---- Output frequency ---- */
int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index a576d918e68d..ac2d977d36f7 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -751,10 +751,112 @@ sit9531x_dpll_output_pin_frequency_set(const struct dpll_pin *pin,
return rc;
}
+/*
+ * sit9531x_dpll_output_pin_state_on_dpll_get - get output pin state
+ *
+ * reports CONNECTED when the output is driven and
+ * DISCONNECTED when it has been muted via sit9531x_output_disable().
+ */
+static int
+sit9531x_dpll_output_pin_state_on_dpll_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state *state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ const struct sit9531x_out *out;
+ int rc;
+
+ /*
+ * A mute whose read-back failed left the cache unconfirmed; there is
+ * no poll of output state to correct it, so read it here rather than
+ * report a value that may predate the request.
+ */
+ if (sitdev->out[dpin->id].state_stale) {
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_output_state_refresh(sitdev, dpin->id);
+ mutex_unlock(&sitdev->multiop_lock);
+ if (rc) {
+ NL_SET_ERR_MSG(extack,
+ "Output mute state could not be read back");
+ return rc;
+ }
+ }
+
+ out = sit9531x_out_state_get(sitdev, dpin->id);
+ *state = out->enabled ? DPLL_PIN_STATE_CONNECTED
+ : DPLL_PIN_STATE_DISCONNECTED;
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_output_pin_state_on_dpll_set - mute/un-mute an output
+ *
+ * Forces Hi-Z on the output through the Hi-Z force/state register pairs
+ * of its slot, the differential and the single-ended one.
+ * CONNECTED -> enable (release the override, the device drives it)
+ * 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;
+ bool was_enabled, changed;
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ was_enabled = sitdev->out[dpin->id].enabled;
+
+ 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;
+ }
+
+ changed = sitdev->out[dpin->id].enabled != was_enabled;
+
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc) {
+ NL_SET_ERR_MSG(extack, "Failed to set output pin state");
+ /*
+ * The core notifies only a request that succeeded, and the
+ * poll does not watch outputs. A failed request whose
+ * read-back shows the output did change still has to be
+ * announced, or subscribers keep the old state for good.
+ * The core's lock is held here, as the helper requires.
+ */
+ if (changed)
+ __dpll_pin_change_ntf(dpin->dpll_pin);
+ }
+
+ return rc;
+}
+
static const struct dpll_pin_ops sit9531x_dpll_output_pin_ops = {
.direction_get = sit9531x_dpll_output_pin_direction_get,
.frequency_get = sit9531x_dpll_output_pin_frequency_get,
.frequency_set = sit9531x_dpll_output_pin_frequency_set,
+ .state_on_dpll_get = sit9531x_dpll_output_pin_state_on_dpll_get,
+ .state_on_dpll_set = sit9531x_dpll_output_pin_state_on_dpll_set,
};
const struct dpll_pin_ops *
--
2.43.0
^ permalink raw reply [flat|nested] 26+ messages in thread* Re: [PATCH net-next v11 10/13] dpll: sit9531x: implement output pin state on a DPLL
2026-09-30 23:37 ` [PATCH net-next v11 10/13] dpll: sit9531x: implement output pin state on a DPLL Ali Rouhi
@ 2026-10-05 1:10 ` netdev-bot+sashiko
0 siblings, 0 replies; 26+ messages in thread
From: netdev-bot+sashiko @ 2026-10-05 1:10 UTC (permalink / raw)
To: arouhi
Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba,
pabeni, robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
devicetree, netdev, linux-kernel
Thank you for your contribution! Sashiko AI review found 3 potential
issue(s) to consider.
Critical: 0 · High: 1 · Medium: 2 · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [High] Possible NULL pointer dereference during device teardown, in
sit9531x_dpll_output_pin_state_on_dpll_set()…
- [Medium] sit9531x_dpll_output_pin_state_on_dpll_set() (dpll.c:822-831)
has no short-circuit when the requested state is already in place.
- [Medium] A state change can go unannounced when the read-back fails.
--
dpll: sit9531x: implement output pin state on a DPLL
This commit adds state_on_dpll_get/set callbacks for output pins. Muting
forces the slot's Hi-Z override bits inside the PRG_CMD programming
sequence, and un-muting releases the override. It also starts registering
output pins that are routed to a given PLL.
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index a576d918e68db..ac2d977d36f7d 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -751,10 +751,112 @@ sit9531x_dpll_output_pin_frequency_set(const struct dpll_pin *pin,
[ ... ]
> +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;
> + bool was_enabled, changed;
> + int rc;
> +
> + mutex_lock(&sitdev->multiop_lock);
> +
> + was_enabled = sitdev->out[dpin->id].enabled;
> +
> + 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;
[Severity: Medium]
Should there be an early return when the request matches the current state?
dpll_pin_state_set() in drivers/dpll/dpll_netlink.c passes every request on
without checking it:
ret = ops->state_on_dpll_set(pin, dpll_pin_on_dpll_priv(dpll, pin),
dpll, dpll_priv(dpll), state, extack);
That means a CONNECTED request on an output that is already driving still
runs the whole sequence:
sit9531x_output_enable()
sit9531x_prg_enter() <- unlocks the output loops, enters PRG_CMD
sit9531x_output_hiz_write() <- clears MASK on every Hi-Z pair
sit9531x_prg_commit() <- UPDATE_NVM, LOOP_LOCK, msleep(100)
All of this runs with multiop_lock and the core's dpll_lock held.
Other paths in this driver skip no-op requests. If the registers already
hold the value, sit9531x_output_phase_adjust_set() skips the rewrite. The
INTSYNC source setter added later in the series skips same-state requests,
with the comment "the core forwards a request for the state the pin is
already in". This output setter is still the same at the end of the
series.
Could this setter check sitdev->out[dpin->id].enabled and state_stale, and
return early when there is nothing to change?
Also, the commit message calls the mute a per-output control that "leaves
the divider alone". It doesn't mention that every request runs the
device-wide programming sequence above.
> + default:
> + rc = -EINVAL;
> + break;
> + }
> +
> + changed = sitdev->out[dpin->id].enabled != was_enabled;
[Severity: Medium]
Can a state change go unannounced when the read-back fails?
Say the Hi-Z writes and sit9531x_prg_commit() succeed, but
sit9531x_output_forced_hiz() fails. Then sit9531x_output_disable() and
sit9531x_output_enable() leave enabled as it was and return the read
error:
} else {
sitdev->out[index].state_stale = true;
if (!rc)
rc = state_rc;
}
Because changed is computed from enabled alone, it is false here and no
notification goes out. The core doesn't notify on error either, from
dpll_pin_state_set():
if (ret)
return ret;
__dpll_pin_change_ntf(pin);
sit9531x_dpll_changes_check() skips output pins. The refresh in
sit9531x_dpll_output_pin_state_on_dpll_get() updates the cache but doesn't
notify anyone.
The comment below says a failed request that changed the output "still has
to be announced, or subscribers keep the old state for good". When
state_stale is set, the mute has most likely landed.
Should state_stale count as a possible change here? Alternatively, the
later refresh could announce the new state if it differs from the cached
one.
> +
> + mutex_unlock(&sitdev->multiop_lock);
> +
> + if (rc) {
> + NL_SET_ERR_MSG(extack, "Failed to set output pin state");
> + /*
> + * The core notifies only a request that succeeded, and the
> + * poll does not watch outputs. A failed request whose
> + * read-back shows the output did change still has to be
> + * announced, or subscribers keep the old state for good.
> + * The core's lock is held here, as the helper requires.
> + */
> + if (changed)
> + __dpll_pin_change_ntf(dpin->dpll_pin);
[Severity: High]
Can dpin->dpll_pin be NULL here if the device is being torn down?
sit9531x_dpll_pin_unregister() clears the field before dpll_lock is taken.
Its comment says callbacks test this field to tell a pin on its way out
from a live one:
pin->dpll_pin = NULL;
dpll_pin_unregister(sitdpll->dpll_dev, dpll_pin, ops, pin);
dpll_pin_pre_doit() holds dpll_lock across this callback. Meanwhile,
sit9531x_output_enable() and sit9531x_output_disable() spend more than
100 ms in I2C transfers and the msleep(100) in sit9531x_prg_commit().
During that window the remove path can clear dpin->dpll_pin and then
block on dpll_lock.
Suppose the request then fails with changed set, for example because the
UPDATE_NVM write fails but the read-back shows the new Hi-Z bits. NULL is
then passed to __dpll_pin_change_ntf(), which dereferences it straight
away:
dpll_pin_notify(pin, pin->clock_id, DPLL_PIN_CHANGED);
The oops would leave dpll_lock held. The blocked unregister and every
later DPLL netlink request would then hang.
Would it be safer to use the pin argument the core passes to this
callback, which stays valid until dpll_pin_unregister() returns? Another
option is to read dpin->dpll_pin once and skip the notification when it is
NULL.
> + }
> +
> + return rc;
> +}
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260930233714.87679-1-arouhi%40sitime.com
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH net-next v11 09/13] dpll: sit9531x: add support to get and set frequency on pins
2026-09-30 23:37 [PATCH net-next v11 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (8 preceding siblings ...)
2026-09-30 23:37 ` [PATCH net-next v11 10/13] dpll: sit9531x: implement output pin state on a DPLL Ali Rouhi
@ 2026-09-30 23:37 ` Ali Rouhi
2026-10-05 1:10 ` netdev-bot+sashiko
2026-09-30 23:37 ` [PATCH net-next v11 12/13] dpll: sit9531x: add support to get phase offset on the connected input pin Ali Rouhi
` (2 subsequent siblings)
12 siblings, 1 reply; 26+ messages in thread
From: Ali Rouhi @ 2026-09-30 23:37 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi, devicetree,
netdev, linux-kernel
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Both directions in one patch, since they share everything that matters.
An input's frequency is what the board presents, so it is reported from
the firmware description rather than read back: the chip has no divider on
an input whose rate it merely qualifies.
An output's frequency is the VCO divided by that output's divider, so it
is computed from the divider read back from the chip and set by writing a
new one. The VCO in turn comes from the feedback divider, which is why
the crystal rate is needed at probe. A divider write only takes effect
inside the programming state, and that state has to be left with the
output loops re-locked whatever happened in between, so the exit runs even
when a write in the middle failed and the first error is the one returned.
Programming a divider costs about a hundred milliseconds under the device
lock: a dozen or so register transactions, then the settling time the part
requires after the loop-lock command, which is a property of the hardware
rather than a conservative guess. The DPLL core holds its own lock across
the whole callback, so a frequency set on this device delays netlink
traffic for every DPLL in the system for that long. Splitting the wait
out would need the ops to complete asynchronously, which the interface
does not offer; issuing the commit without waiting would let the next
request program a part that has not settled. A rate change is a
configuration action, not something a running system does per packet, so
the cost is paid where it is visible rather than hidden behind a
completion the caller cannot wait for.
The phase flush that follows a divider write realigns every output fed by
that PLL, not only the one that changed. The flush is a per-PLL function
in the device and there is no per-output equivalent, so an output whose
rate is set while its siblings are running will step their phase too.
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
drivers/dpll/sit9531x/core.c | 778 +++++++++++++++++++++++++++++++++++
drivers/dpll/sit9531x/core.h | 4 +
drivers/dpll/sit9531x/dpll.c | 95 +++++
drivers/dpll/sit9531x/prop.c | 28 +-
drivers/dpll/sit9531x/regs.h | 21 +
5 files changed, 922 insertions(+), 4 deletions(-)
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index f6c76cc78456..3254d119b939 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -488,6 +488,139 @@ static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev,
muted);
}
+/* Attempts to re-lock the output loops before reporting them open. */
+#define SIT9531X_LOOP_LOCK_TRIES 3
+
+/*
+ * sit9531x_prg_abort - leave the programming state without committing
+ *
+ * Entering the state is two writes, and the second can fail with the debug
+ * block already unlocked and the part possibly already in PRG_CMD. There
+ * is nothing to commit in that case, but the loops still have to be closed
+ * and the debug key put back, which is otherwise only done by
+ * sit9531x_prg_commit().
+ */
+static void sit9531x_prg_abort(struct sit9531x_dev *sitdev)
+{
+ u8 attempt;
+ int rc = -EIO;
+
+ for (attempt = 0; attempt < SIT9531X_LOOP_LOCK_TRIES; attempt++) {
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+ SIT9531X_LOOP_LOCK);
+ if (!rc)
+ break;
+ usleep_range(1000, 2000);
+ }
+ if (rc)
+ dev_err(sitdev->dev,
+ "output loops left unlocked after a failed entry: %d\n",
+ rc);
+
+ sit9531x_write_u8(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
+ SIT9531X_DEBUG_LOCK_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;
+
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+ SIT9531X_PRG_CMD_STATE);
+ if (rc) {
+ /*
+ * The debug block is unlocked at this point, and a transfer
+ * that reported an error may still have reached the part --
+ * which would leave the device in PRG_CMD with its output
+ * loops open. Callers skip the commit when the entry
+ * fails, so close both here.
+ */
+ sit9531x_prg_abort(sitdev);
+ return rc;
+ }
+
+ return 0;
+}
+
+/*
+ * Commit a programming sequence started by sit9531x_prg_enter():
+ * update the NVM shadow and re-lock the loops. The sleep gives the
+ * hardware its required settling time after the loop-lock command;
+ * it is intentional despite the caller holding multiop_lock, as the
+ * whole NVM + lock sequence must be atomic.
+ */
+static int sit9531x_prg_commit(struct sit9531x_dev *sitdev)
+{
+ int rc, rc2 = 0, rc3;
+ u8 attempt;
+
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+ SIT9531X_UPDATE_NVM);
+
+ /*
+ * Issue the loop lock even if the update failed: every caller
+ * commits whatever happened after the entry so that the chip never
+ * stays in the PRG_CMD state with its loops open, and returning
+ * early here would defeat that.
+ *
+ * Re-lock the loops. Leaving them open is worse than any other
+ * failure this function can report, and nothing else closes them,
+ * so retry as the priority table does with its own latch.
+ */
+ for (attempt = 0; attempt < SIT9531X_LOOP_LOCK_TRIES; attempt++) {
+ rc2 = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+ SIT9531X_LOOP_LOCK);
+ if (!rc2)
+ break;
+ usleep_range(1000, 2000);
+ }
+ if (rc2)
+ dev_err(sitdev->dev,
+ "output loops left unlocked after programming: %d\n",
+ rc2);
+
+ msleep(100);
+
+ /*
+ * Put the output-system debug block back the way the device powers
+ * up. Its key register unlocks every debug register while it holds
+ * the unlock value, and each programming sequence writes that value
+ * itself, so nothing needs it left unlocked in between.
+ */
+ rc3 = sit9531x_write_u8(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
+ SIT9531X_DEBUG_LOCK_VAL);
+
+ if (rc)
+ return rc;
+ if (rc2)
+ return rc2;
+
+ /*
+ * The programming is committed and the loops are locked by now;
+ * only the debug key stayed open, and the next sequence writes it
+ * again. Failing here would make the caller skip what follows a
+ * change that did take effect -- and the core drops an identical
+ * retry, so it would never run.
+ */
+ if (rc3)
+ dev_warn(sitdev->dev,
+ "output debug block left unlocked after programming: %d\n",
+ rc3);
+
+ return 0;
+}
+
/*
* Input priority selection
*
@@ -1252,6 +1385,11 @@ int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
chan->prio_mask | BIT(input_idx));
}
+/* 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 */
@@ -1265,6 +1403,646 @@ int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
/* The output divider is a 34-bit field */
#define SIT9531X_DIVO_MAX GENMASK_ULL(33, 0)
+/*
+ * sit9531x_is_xo_doubler_enabled - check if Fref doubler is active
+ *
+ * Register 0x2D bit 7 is active-low: 0 = doubler enabled, 1 = disabled.
+ *
+ * Return: 1 if enabled, 0 if disabled, <0 on error
+ */
+static int sit9531x_is_xo_doubler_enabled(struct sit9531x_dev *sitdev)
+{
+ u8 val;
+ int rc;
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_XO2_GENERIC, &val);
+ if (rc)
+ return rc;
+
+ return (~val >> SIT9531X_XO_DOUBLER_ENB_BIT) & 1u;
+}
+
+/*
+ * DIVN as a fixed-point value: int_part plus fracn/fracd, carried with
+ * SIT9531X_DIVN_SCALE steps per unit. The scale keeps a whole DIVN
+ * well inside s64 while resolving far below the parts-per-trillion the
+ * frequency offset is reported in.
+ */
+static s64 sit9531x_divn_fixed(u32 int_part, s64 fracn, u64 fracd)
+{
+ s64 whole = (s64)int_part * SIT9531X_DIVN_SCALE;
+ u64 frac;
+
+ if (!fracd)
+ return whole;
+
+ frac = mul_u64_u64_div_u64(abs(fracn), SIT9531X_DIVN_SCALE, fracd);
+
+ return fracn < 0 ? whole - (s64)frac : whole + (s64)frac;
+}
+
+/*
+ * sit9531x_divn_static - read the configured DIVN of a PLL
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ * @divn: result, fixed point as per sit9531x_divn_fixed()
+ *
+ * Reads PLL page regs 0x30 (integer part), 0x32-0x35 (numerator) and
+ * 0x38-0x3B (denominator). The numerator is a two's complement 32-bit
+ * value, so DIVN can sit below the integer part, and the denominator
+ * register holds the divisor minus one.
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int sit9531x_divn_static(struct sit9531x_dev *sitdev, u8 pll_idx,
+ s64 *divn)
+{
+ u32 int_part, fracn_raw = 0, fracd_raw = 0;
+ u64 fracd;
+ s64 fracn;
+ int rc, i;
+ u8 v;
+
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DIVN_INT, &v);
+ if (rc)
+ return rc;
+ int_part = v;
+
+ for (i = 3; i >= 0; i--) {
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DIVN_NUM + i, &v);
+ if (rc)
+ return rc;
+ fracn_raw = (fracn_raw << 8) | v;
+ }
+
+ for (i = 3; i >= 0; i--) {
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DIVN_DEN + i, &v);
+ if (rc)
+ return rc;
+ fracd_raw = (fracd_raw << 8) | v;
+ }
+
+ /*
+ * NUM/DEN is the fractional part of DIVN, so |NUM| is below DEN by
+ * construction. A pair that says otherwise did not come from a
+ * programmed divider, and handing it on would divide by a
+ * denominator small enough for the quotient to leave u64 -- which
+ * is a divide-error exception on x86, not a value a caller could
+ * reject.
+ */
+ fracn = (s32)fracn_raw;
+ fracd = (u64)fracd_raw + 1;
+ if ((u64)abs(fracn) >= fracd)
+ return -ENODATA;
+
+ *divn = sit9531x_divn_fixed(int_part, fracn, fracd);
+
+ return 0;
+}
+
+/*
+ * sit9531x_get_fvco - read VCO frequency from chip's DIVN registers
+ *
+ * Fvco = Fref * DIVN, where DIVN comes from sit9531x_divn_static() and
+ * Fref = xtal_freq << doubler. DIVN is the steady-state Fvco/Fref
+ * target programmed by the NVM blob and is authoritative in both
+ * free-run and sync modes.
+ *
+ * Return: 0 with *fvco set on success, -ENODATA when DIVN is not
+ * programmed (dormant PLL), or the register access error. A bus
+ * failure is never folded into the -ENODATA case, so callers can fail
+ * a request instead of acting on a guessed rate.
+ */
+static int sit9531x_get_fvco(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u64 *fvco)
+{
+ u64 fref, fvco_min, fvco_max;
+ int doubler, rc;
+ s64 divn;
+
+ if (pll_idx == 1 || pll_idx == 3) {
+ /* PLLB, PLLD: high band */
+ fvco_min = SIT9531X_FVCO_HIGHBAND_MIN;
+ fvco_max = SIT9531X_FVCO_HIGHBAND_MAX;
+ } else {
+ /* PLLA, PLLC: low band */
+ fvco_min = SIT9531X_FVCO_LOWBAND_MIN;
+ fvco_max = SIT9531X_FVCO_LOWBAND_MAX;
+ }
+
+ rc = sit9531x_divn_static(sitdev, pll_idx, &divn);
+ if (rc)
+ return rc;
+ if (divn <= 0)
+ return -ENODATA;
+
+ doubler = sit9531x_is_xo_doubler_enabled(sitdev);
+ if (doubler < 0)
+ return doubler;
+
+ fref = (u64)sitdev->xtal_freq << doubler;
+
+ /*
+ * Round to the nearest hertz: DIVN is carried in fixed point, so a
+ * fraction such as 1/6 is already floored once, and flooring the
+ * product again reads an exact 5 GHz VCO as 4999999999 Hz -- which
+ * then refuses every rate the VCO divides exactly.
+ */
+ *fvco = mul_u64_u64_div_u64(2 * fref, (u64)divn, SIT9531X_DIVN_SCALE);
+ *fvco = (*fvco + 1) / 2;
+
+ /*
+ * A DIVN of less than one whole cycle passes the check above and
+ * still truncates the product to zero. Callers divide by this, so
+ * report the unprogrammed divider it describes rather than handing
+ * back a zero denominator.
+ */
+ if (!*fvco)
+ return -ENODATA;
+
+ /*
+ * A rate outside the band the PLL's VCO runs in usually means
+ * Fref * DIVN is not what this PLL runs at -- a PLL fed from another
+ * PLL rather than from the XO, for one. The rate derived from the
+ * registers is still the only estimate there is, so it is used as
+ * it is: substituting the band edge would program dividers against
+ * a rate nothing supports and report the result as exact. Say so
+ * once.
+ */
+ if (*fvco < fvco_min || *fvco > fvco_max)
+ dev_warn_once(sitdev->dev,
+ "PLL%c: Fref * DIVN = %llu Hz is outside its VCO band\n",
+ 'A' + pll_idx, *fvco);
+
+ return 0;
+}
+
+/* Latch a change to a PLL page with a small update. */
+static int sit9531x_pll_small_update(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+ return sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_SMALL_UPDATE,
+ SIT9531X_SMALL_UPDATE_CMD);
+}
+
+/*
+ * sit9531x_output_phase_flush - flush the output phase of a PLL
+ *
+ * Fires the chip's on-demand phase-flush (PHFL) so every output divider
+ * of @pll_idx restarts aligned to the PLL phase. Without it a rewritten
+ * DIVO keeps counting from an arbitrary point and the output edge lands
+ * with a persistent offset against the tracked reference (only a power
+ * cycle realigned it).
+ *
+ * The sequence mirrors the documented procedure: arm the on-demand PHFL and
+ * latch it with the PLL-page small-change update, then select the
+ * in-register phase trigger on Page 0 and pulse it. The Page 0 trigger
+ * register is touched read-modify-write so the unrelated OEb bits are
+ * preserved.
+ *
+ * A PLL the loaded configuration builds without the phase-flush feature
+ * (PLL page 0x47 bit 7 clear) has nothing to fire; it is restarted
+ * instead, which restarts its output dividers from the PLL phase. That
+ * is what SiTime's Dely_program_output.py does for such a PLL.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_output_phase_flush(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+ u8 ctrl, orig, phfl, armed_mask = 0, parked = 0, i;
+ int rc, ret;
+
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_CONFIG47,
+ &phfl);
+ if (rc)
+ return rc;
+ if (!(phfl & SIT9531X_PLL_CONFIG47_PHFL_EN))
+ return sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DIRECTIVES,
+ SIT9531X_PLL_DIRECTIVE_RESTART);
+
+ /*
+ * The trigger below is chip-global: every PLL whose on-demand flush
+ * is armed answers it. Note which ones the loaded profile arms --
+ * this one so its setting can be put back, the others so they can
+ * be parked for the pulse and do not realign outputs nobody asked
+ * to move.
+ */
+ for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+ rc = sit9531x_read_pll_u8(sitdev, i, SIT9531X_PLL_REG_PHFL_CTRL,
+ &phfl);
+ if (rc)
+ return rc;
+ if (phfl & SIT9531X_PLL_PHFL_ON_DEMAND_EN)
+ armed_mask |= BIT(i);
+ }
+
+ for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+ if (i == pll_idx || !(armed_mask & BIT(i)))
+ continue;
+ rc = sit9531x_update_pll_u8(sitdev, i,
+ SIT9531X_PLL_REG_PHFL_CTRL,
+ SIT9531X_PLL_PHFL_ON_DEMAND_EN, 0);
+ if (rc)
+ goto unpark;
+ /* Cleared, if not latched yet: put back either way. */
+ parked |= BIT(i);
+ rc = sit9531x_pll_small_update(sitdev, i);
+ if (rc)
+ goto unpark;
+ }
+
+ /* 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)
+ goto unpark;
+
+ /*
+ * Latch it with the PLL small-change update. Written whole, like
+ * every other issue of this directive: the register is a command
+ * register, and a read-modify-write skips the write entirely when
+ * the bit still reads back set from the previous command.
+ */
+ rc = sit9531x_pll_small_update(sitdev, pll_idx);
+ if (rc)
+ goto disarm;
+
+ /*
+ * Select the in-register phase trigger, preserving the OEb bits.
+ * Remember the original register value (with the trigger de-asserted)
+ * so the trigger-source select can be restored once the pulse has
+ * fired.
+ */
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, &ctrl);
+ if (rc)
+ goto disarm;
+
+ orig = ctrl & ~SIT9531X_DIVO_PHASE_TRIG;
+ ctrl = orig | SIT9531X_DIVO_PHASE_SEL_REG;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, ctrl);
+ if (rc)
+ goto disarm;
+
+ /*
+ * Pulse the phase trigger. No explicit delay is needed between the
+ * set and clear writes: each I2C transaction takes far longer than
+ * any minimum pulse width.
+ */
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1,
+ ctrl | SIT9531X_DIVO_PHASE_TRIG);
+
+ /*
+ * Restore the original trigger-source select. The pulse above has
+ * already latched the flush, so a one-shot flush must not leave the
+ * phase trigger permanently pinned to the in-register source. This
+ * runs even when the pulse write failed, otherwise a failed flush
+ * would keep a hardware trigger source hijacked; the restore error
+ * is only surfaced when it would not mask the pulse failure.
+ */
+ ret = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, orig);
+ if (ret && !rc)
+ rc = ret;
+
+disarm:
+ /*
+ * Put the on-demand flush enable back as the profile had it. One the
+ * profile left clear must not stay armed: a later assertion of the
+ * restored trigger source would re-flush every output divider of this
+ * PLL. One the profile armed -- a board realigning from a GPIO, say
+ * -- must not be switched off by an unrelated rate change.
+ */
+ if (!(armed_mask & BIT(pll_idx))) {
+ ret = sit9531x_update_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_PHFL_CTRL,
+ SIT9531X_PLL_PHFL_ON_DEMAND_EN, 0);
+ if (!ret)
+ ret = sit9531x_pll_small_update(sitdev, pll_idx);
+ if (ret && !rc)
+ rc = ret;
+ }
+
+unpark:
+ for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+ if (!(parked & BIT(i)))
+ continue;
+ ret = sit9531x_update_pll_u8(sitdev, i,
+ SIT9531X_PLL_REG_PHFL_CTRL,
+ SIT9531X_PLL_PHFL_ON_DEMAND_EN,
+ SIT9531X_PLL_PHFL_ON_DEMAND_EN);
+ if (!ret)
+ ret = sit9531x_pll_small_update(sitdev, i);
+ if (ret && !rc)
+ rc = ret;
+ }
+
+ return rc;
+}
+
+/*
+ * sit9531x_output_divo_calc - work out an output's divider and its VCO
+ *
+ * Separated from the write so a caller that programs more than the
+ * divider in one sequence can compute the value before it enters the
+ * programming state.
+ */
+static int sit9531x_output_divo_calc(struct sit9531x_dev *sitdev, u8 out_idx,
+ u8 pll_idx, u64 frequency, u64 *fvco_out,
+ u64 *divo_out)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ u64 fvco, divo;
+ int rc;
+
+ if (out_idx >= info->num_outputs || pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+
+ if (!frequency)
+ return -EINVAL;
+
+ /*
+ * The core validates the request against the supported ranges with
+ * the value narrowed to u32 but hands the full u64 down, so a value
+ * like U32_MAX + 1 Hz validates as 1 Hz. Reject anything that does
+ * not fit the narrowed width the validation actually covered.
+ */
+ if (frequency > U32_MAX)
+ return -EINVAL;
+
+ /*
+ * sit9531x_get_fvco() returns the register-derived rate, so a
+ * frequency get and a frequency set divide the same number. A VCO
+ * that cannot be read fails the request: programming a divider from a
+ * guessed rate would put the output far from what was asked for
+ * while reporting success.
+ */
+ rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
+ if (rc)
+ return rc == -ENODATA ? -ENODEV : rc;
+
+ /*
+ * Round to nearest rather than down: flooring picks the worse of the
+ * two adjacent dividers whenever the remainder is above half the
+ * request.
+ */
+ divo = div64_u64(fvco + frequency / 2, frequency);
+ if (!divo)
+ return -EINVAL;
+
+ /*
+ * DIVO is a 34-bit field. Fvco is used as it is, even outside the
+ * VCO band, so a divider can come out wider than the field; refuse
+ * it rather than truncate it.
+ */
+ if (divo > SIT9531X_DIVO_MAX)
+ return -EINVAL;
+
+ /*
+ * The output divider is an integer divider of the VCO, so the only
+ * rates the part can make are Fvco/N. An output pin that lists no
+ * supported frequencies advertises a continuous range, because the
+ * divisors cannot be enumerated ahead of a known Fvco, so a request
+ * for a rate between two of them arrives here. Refuse it: running
+ * the output at the nearest divider instead and reporting success
+ * would leave the pin several percent off what was asked for with
+ * nothing saying so.
+ */
+ if (div64_u64(fvco, divo) != frequency) {
+ dev_dbg(sitdev->dev,
+ "out%u: %llu Hz is not Fvco/N (Fvco=%llu, nearest %llu Hz)\n",
+ out_idx, frequency, fvco, div64_u64(fvco, 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));
+
+ *fvco_out = fvco;
+ *divo_out = divo;
+
+ return 0;
+}
+
+/*
+ * sit9531x_output_divo_write - write the five DIVO bytes of an output
+ *
+ * The caller must already be in the programming state. Bytes written
+ * before a failure are put back, so the output keeps the divider it had
+ * rather than a mixture of the two.
+ */
+static int sit9531x_output_divo_write(struct sit9531x_dev *sitdev, u8 out_idx,
+ u64 divo)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ u8 slot, page, base_reg, divo_bytes[5], old_bytes[5], msb_old;
+ int rc, j, rb_rc;
+ u8 written = 0;
+
+ /* Map output index to physical slot */
+ slot = info->clkout_map[out_idx];
+
+ /* Determine page and per-page slot register */
+ if (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX)
+ page = SIT9531X_PAGE_OUTSYS1;
+ else
+ page = SIT9531X_PAGE_OUTSYS0;
+ base_reg = clkout_odr_divn_base[slot % 6];
+
+ divo_bytes[0] = (divo >> 0) & 0xFF;
+ divo_bytes[1] = (divo >> 8) & 0xFF;
+ divo_bytes[2] = (divo >> 16) & 0xFF;
+ divo_bytes[3] = (divo >> 24) & 0xFF;
+ divo_bytes[4] = (divo >> 32) & 0x03; /* only bits [1:0] */
+
+ for (j = 0; j < 5; j++) {
+ rc = sit9531x_read_u8(sitdev,
+ SIT9531X_REG(page, base_reg - j),
+ &old_bytes[j]);
+ if (rc)
+ return rc;
+ }
+
+ msb_old = old_bytes[4];
+ divo_bytes[4] |= msb_old & 0xFC;
+
+ for (j = 0; j < 5; j++) {
+ rc = sit9531x_write_u8(sitdev,
+ SIT9531X_REG(page, base_reg - j),
+ divo_bytes[j]);
+ if (rc)
+ goto rollback;
+ written++;
+ }
+
+ return 0;
+
+rollback:
+ /*
+ * The byte whose write reported the error may still have reached
+ * the part, so it is put back along with the ones that did.
+ */
+ for (j = 0; j <= written && j < 5; j++) {
+ rb_rc = sit9531x_write_u8(sitdev,
+ SIT9531X_REG(page, base_reg - j),
+ old_bytes[j]);
+ if (rb_rc) {
+ dev_err(sitdev->dev,
+ "out%u: DIVO rollback failed (%d), the divider is part old and part new\n",
+ out_idx, rb_rc);
+ if (!rc)
+ rc = rb_rc;
+ }
+ }
+
+ return rc;
+}
+
+/*
+ * Read an output's divider back from the device.
+ *
+ * Return: 0 with *divo set, -ENODATA when the divider is not programmed,
+ * or the register access error
+ */
+static int sit9531x_output_divo_read(struct sit9531x_dev *sitdev, u8 out_idx,
+ u64 *divo)
+{
+ u8 slot, page, base_reg, v;
+ int rc, j;
+
+ slot = sitdev->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];
+
+ *divo = 0;
+ 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;
+ }
+
+ return *divo ? 0 : -ENODATA;
+}
+
+int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
+ u8 pll_idx, u64 frequency)
+{
+ u64 fvco, divo;
+ int rc, ret;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ rc = sit9531x_output_divo_calc(sitdev, out_idx, pll_idx, frequency,
+ &fvco, &divo);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_prg_enter(sitdev);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_output_divo_write(sitdev, out_idx, divo);
+ /*
+ * Step 4: NVM update + loop lock. Always run prg_commit() so the chip
+ * leaves the PRG_CMD state with the output loops re-locked, even when a
+ * write above failed; keep the first error to return. It also carries
+ * the required post-lock settling sleep.
+ */
+ ret = sit9531x_prg_commit(sitdev);
+ if (ret && !rc)
+ rc = ret;
+ if (rc)
+ return rc;
+
+ /*
+ * Step 5: flush the PLL's output phase so the new DIVO starts
+ * aligned instead of keeping the arbitrary phase the divider
+ * happened to be at.
+ */
+ /*
+ * The divider is committed by this point, so the part is already
+ * running at the new rate. A flush that fails leaves the output
+ * divider on its old phase, which is a realignment that did not
+ * happen rather than a rate that did not change -- and reporting a
+ * failure would be doubly wrong, because the core asks for the
+ * current rate first and would drop an identical retry.
+ */
+ rc = sit9531x_output_phase_flush(sitdev, pll_idx);
+ if (rc) {
+ dev_warn(sitdev->dev,
+ "out%u: rate changed but the divider phase was not realigned (%d)\n",
+ out_idx, rc);
+ rc = 0;
+ }
+
+ sitdev->out[out_idx].freq = div64_u64(fvco, divo);
+
+ return 0;
+}
+
+/*
+ * sit9531x_output_freq_get - read output clock frequency from hardware
+ * @out_idx: output index (0-N for this chip variant)
+ * @frequency: output frequency in Hz
+ *
+ * Reads the 34-bit DIVO divider back from the output system registers
+ * and computes the live output frequency as Fvco / DIVO. This stays
+ * correct even when the divider was reprogrammed behind the driver's
+ * back (e.g. by a direct-I2C userspace tool), where the cached value
+ * would be stale.
+ *
+ * The cached output state is refreshed with the computed value.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, -ENODATA when the output divider or VCO rate
+ * is not resolvable, <0 on register access error. -ENODATA and not
+ * -ENODEV, which the I2C layer returns for an adapter that is gone
+ * and which must not read as "no rate to report".
+ */
+int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
+ u64 *frequency)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ u64 fvco, divo;
+ u8 pll_idx;
+ int rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (out_idx >= info->num_outputs)
+ return -EINVAL;
+
+ pll_idx = sitdev->out[out_idx].pll_idx;
+ if (pll_idx >= SIT9531X_NUM_PLLS || !sitdev->out[out_idx].routed)
+ return -ENODATA;
+
+ rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_output_divo_read(sitdev, out_idx, &divo);
+ if (rc)
+ return rc;
+
+ *frequency = div64_u64(fvco, divo);
+ sitdev->out[out_idx].freq = *frequency;
+
+ return 0;
+}
+
/*
* Phase adjust (PRG_RST_DELAY register-based).
*
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 8aa505f84e87..473171a19d94 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -260,6 +260,10 @@ int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
/* ---- Output enable/disable (Hi-Z control) ---- */
/* ---- 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) ---- */
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index ebbf7f5a3394..a576d918e68d 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -350,6 +350,29 @@ sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
return 0;
}
+/*
+ * sit9531x_dpll_input_pin_frequency_get - read input pin frequency
+ *
+ * Returns the rate the board wired to the input, the first entry of its
+ * supported-frequencies-hz; an input has no frequency setter.
+ */
+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
*
@@ -584,6 +607,7 @@ sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
.direction_get = sit9531x_dpll_input_pin_direction_get,
+ .frequency_get = sit9531x_dpll_input_pin_frequency_get,
.state_on_dpll_get = sit9531x_dpll_input_pin_state_on_dpll_get,
.state_on_dpll_set = sit9531x_dpll_input_pin_state_on_dpll_set,
.operstate_on_dpll_get = sit9531x_dpll_input_pin_operstate_on_dpll_get,
@@ -643,6 +667,7 @@ sit9531x_dpll_xo_pin_state_on_dpll_get(const struct dpll_pin *pin,
static const struct dpll_pin_ops sit9531x_dpll_xo_pin_ops = {
.direction_get = sit9531x_dpll_input_pin_direction_get,
+ .frequency_get = sit9531x_dpll_input_pin_frequency_get,
.state_on_dpll_get = sit9531x_dpll_xo_pin_state_on_dpll_get,
};
@@ -658,8 +683,78 @@ sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
return 0;
}
+/*
+ * sit9531x_dpll_output_pin_frequency_get - read output pin frequency
+ *
+ * Reads the DIVO divider back from the chip and computes the live
+ * frequency as Fvco / DIVO. Falls back to the cached value only when
+ * the output is not resolvable through the divider chain (e.g. not
+ * mapped to a PLL), so transport/register errors still surface.
+ */
+static int
+sit9531x_dpll_output_pin_frequency_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, u64 *frequency,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_output_freq_get(sitdev, dpin->id, frequency);
+ if (rc == -ENODATA)
+ *frequency = sit9531x_out_state_get(sitdev, dpin->id)->freq;
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return rc == -ENODATA ? 0 : rc;
+}
+
+/*
+ * sit9531x_dpll_output_pin_frequency_set - set output pin frequency
+ *
+ * computes DIVO = Fvco / frequency and writes the
+ * 34-bit output divider to the output system registers via
+ * sit9531x_output_freq_set().
+ */
+static int
+sit9531x_dpll_output_pin_frequency_set(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, u64 frequency,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ u8 actual_pll;
+ int rc;
+
+ /*
+ * Read the PLL that drives this output from its OUT_MAP state
+ * (populated by out_state_fetch from the chip's OUT_MAP registers).
+ * That is the index the output register programming below is keyed
+ * by; the output is registered under the DPLL matching this PLL.
+ */
+ actual_pll = sitdev->out[dpin->id].pll_idx;
+
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll,
+ frequency);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc)
+ NL_SET_ERR_MSG(extack, "Output frequency set failed");
+
+ return rc;
+}
+
static const struct dpll_pin_ops sit9531x_dpll_output_pin_ops = {
.direction_get = sit9531x_dpll_output_pin_direction_get,
+ .frequency_get = sit9531x_dpll_output_pin_frequency_get,
+ .frequency_set = sit9531x_dpll_output_pin_frequency_set,
};
const struct dpll_pin_ops *
diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
index 934db566c002..42f3e53b6def 100644
--- a/drivers/dpll/sit9531x/prop.c
+++ b/drivers/dpll/sit9531x/prop.c
@@ -295,15 +295,35 @@ sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
}
/*
- * Seed an input's runtime ref->freq with the first DT-listed
- * supported frequency: for an input the board lists the rate that is
- * physically wired to it first. An output's current rate is what its
- * divider produces, not an entry of the list it may be set to.
+ * Seed the runtime ref->freq with the first DT-listed supported
+ * frequency: an input's rate is a board fact, so firmware is the
+ * source. An output is left to the read-back below, which knows
+ * what the divider is actually doing.
*/
if (num_freqs > 0 && dir == DPLL_PIN_DIRECTION_INPUT &&
index != SIT9531X_MAX_INPUTS)
curr_freq = freqs[0];
+ /*
+ * An output's current rate is the one its divider produces, so read
+ * it rather than assume the first entry of a list of the rates the
+ * board supports is the one in force. A rate taken from firmware
+ * that the part is not running would be reported as current and,
+ * worse, used as the output period the phase adjust quantizes
+ * against. An output the configuration does not route has no rate
+ * to read, which is not an error.
+ */
+ if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+ index < sitdev->info->num_outputs) {
+ u64 hw_freq;
+
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_output_freq_get(sitdev, index, &hw_freq);
+ mutex_unlock(&sitdev->multiop_lock);
+ if (!rc)
+ curr_freq = hw_freq;
+ }
+
skip_fwnode_props:
/* Neither INTSYNC pin carries a frequency attribute */
if (dir == DPLL_PIN_DIRECTION_INPUT &&
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index d5e378715110..d2e1ac547cbc 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -200,6 +200,7 @@
/* Debug register (same offset, per-page) */
#define SIT9531X_REG_OUTSYS_DEBUG SIT9531X_REG(0x03, 0xBD)
#define SIT9531X_DEBUG_UNLOCK_VAL 0xC3
+#define SIT9531X_DEBUG_LOCK_VAL 0x00
/*
* On-demand phase-flush fired from a register rather than a GPIO pin.
@@ -214,6 +215,18 @@
/* ---- 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)
+
+/* Whether the PLL has the phase-flush feature (PLL page reg 0x47 bit 7) */
+#define SIT9531X_PLL_REG_CONFIG47 0x47
+#define SIT9531X_PLL_CONFIG47_PHFL_EN BIT(7)
+
+/* Directives_GENERIC_PLL: the restart bit restarts the PLL, self-clearing */
+#define SIT9531X_PLL_REG_DIRECTIVES 0x05
+#define SIT9531X_PLL_DIRECTIVE_RESTART BIT(0)
+
/*
* 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
@@ -229,6 +242,14 @@
#define SIT9531X_PLL_REG_OUT_MAP_LO 0x28
#define SIT9531X_PLL_REG_STATUS 0x31
+/* DIVN registers (free-run divider readback) */
+#define SIT9531X_PLL_REG_DIVN_INT 0x30
+#define SIT9531X_PLL_REG_DIVN_NUM 0x32 /* 4 bytes (0x32-0x35) */
+#define SIT9531X_PLL_REG_DIVN_DEN 0x38 /* 4 bytes (0x38-0x3B) */
+
+/* DIVN is carried as fixed point, in steps of 1e-12 of a whole divider */
+#define SIT9531X_DIVN_SCALE 1000000000000ULL
+
#define SIT9531X_PLL_REG_ACTIVE 0x02
#define SIT9531X_PLL_ACTIVE_BIT BIT(0) /* PLL reached active state */
--
2.43.0
^ permalink raw reply [flat|nested] 26+ messages in thread* Re: [PATCH net-next v11 09/13] dpll: sit9531x: add support to get and set frequency on pins
2026-09-30 23:37 ` [PATCH net-next v11 09/13] dpll: sit9531x: add support to get and set frequency on pins Ali Rouhi
@ 2026-10-05 1:10 ` netdev-bot+sashiko
0 siblings, 0 replies; 26+ messages in thread
From: netdev-bot+sashiko @ 2026-10-05 1:10 UTC (permalink / raw)
To: arouhi
Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba,
pabeni, robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
devicetree, netdev, linux-kernel
Thank you for your contribution! Sashiko AI review found 6 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 5 · Low: 1
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] sit9531x_dpll_input_pin_frequency_get() always returns
`ref->freq` and succeeds.
- [Medium] The new input frequency_get reports sitdev->ref[index].freq.
- [Medium] Elsewhere in this patch (prg_enter, divo_write rollback) the
driver assumes that an I2C write which reports an error 'may still…
- [Medium] sit9531x_prg_abort() runs when the PRG_CMD_STATE write in
sit9531x_prg_enter() reports an error.
- [Medium] sit9531x_get_fvco() detects that Fref*DIVN lies outside the
PLL's VCO band.
- [Low] The sit9531x_prg_commit() comment justifies its LOOP_LOCK retry
loop with 'retry as the priority table does with its own latch'.
--
dpll: sit9531x: add support to get and set frequency on pins
This adds frequency_get for the input and XO pins, which reports the rate
from the firmware description. It also adds frequency_get and
frequency_set for the output pins, computed as Fvco / DIVO. An output rate
change writes DIVO inside the output-system programming state, commits it
with an NVM update and a loop lock, and then fires a per-PLL phase flush.
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index f6c76cc78456d..3254d119b9398 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -488,6 +488,139 @@ static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev,
[ ... ]
> +static void sit9531x_prg_abort(struct sit9531x_dev *sitdev)
> +{
> + u8 attempt;
> + int rc = -EIO;
> +
> + for (attempt = 0; attempt < SIT9531X_LOOP_LOCK_TRIES; attempt++) {
> + rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
> + SIT9531X_LOOP_LOCK);
> + if (!rc)
> + break;
> + usleep_range(1000, 2000);
> + }
> + if (rc)
> + dev_err(sitdev->dev,
> + "output loops left unlocked after a failed entry: %d\n",
> + rc);
> +
> + sit9531x_write_u8(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
> + SIT9531X_DEBUG_LOCK_VAL);
> +}
[Severity: Medium]
Should this path also wait out the settling time after the loop lock?
sit9531x_prg_commit() follows the same LOOP_LOCK write with msleep(100).
The commit message calls that wait "a property of the hardware rather
than a conservative guess". It also says that skipping it "would let the
next request program a part that has not settled".
This abort runs when the PRG_CMD_STATE write in sit9531x_prg_enter()
reported an error. By the comment in sit9531x_prg_enter(), that write may
still have reached the part. sit9531x_output_freq_set() then returns
straight away:
rc = sit9531x_prg_enter(sitdev);
if (rc)
return rc;
The caller then drops multiop_lock. The next frequency, phase-adjust or
priority request can then start programming while the output loops are
still re-locking.
The abort path also never issues SIT9531X_UPDATE_NVM. Is LOOP_LOCK on its
own a valid way to leave PRG_CMD on this part?
[ ... ]
> +static int sit9531x_prg_commit(struct sit9531x_dev *sitdev)
> +{
> + int rc, rc2 = 0, rc3;
> + u8 attempt;
> +
> + rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
> + SIT9531X_UPDATE_NVM);
> +
> + /*
> + * Issue the loop lock even if the update failed: every caller
> + * commits whatever happened after the entry so that the chip never
> + * stays in the PRG_CMD state with its loops open, and returning
> + * early here would defeat that.
> + *
> + * Re-lock the loops. Leaving them open is worse than any other
> + * failure this function can report, and nothing else closes them,
> + * so retry as the priority table does with its own latch.
> + */
[Severity: Low]
This isn't a bug, but is this comment accurate? The priority table's
latch is sit9531x_prio_prg_commit(), which writes once and does not
retry:
rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
SIT9531X_SMALL_UPDATE_CMD);
if (rc)
return rc;
The only retry in sit9531x_prio_table_commit() is the loop that releases
the forced holdover, bounded by SIT9531X_HO_CLEAR_TRIES.
[ ... ]
> @@ -1265,6 +1403,646 @@ int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
[ ... ]
> +static int sit9531x_get_fvco(struct sit9531x_dev *sitdev, u8 pll_idx,
> + u64 *fvco)
> +{
[ ... ]
> + /*
> + * A rate outside the band the PLL's VCO runs in usually means
> + * Fref * DIVN is not what this PLL runs at -- a PLL fed from another
> + * PLL rather than from the XO, for one. The rate derived from the
> + * registers is still the only estimate there is, so it is used as
> + * it is: substituting the band edge would program dividers against
> + * a rate nothing supports and report the result as exact. Say so
> + * once.
> + */
> + if (*fvco < fvco_min || *fvco > fvco_max)
> + dev_warn_once(sitdev->dev,
> + "PLL%c: Fref * DIVN = %llu Hz is outside its VCO band\n",
> + 'A' + pll_idx, *fvco);
> +
> + return 0;
> +}
[Severity: Medium]
Can this make frequency_set report success for a rate the output is not
running at?
When Fref * DIVN is out of band, the comment says it is probably not what
the PLL runs at, but the function still returns 0.
sit9531x_output_divo_calc() then computes DIVO from that value, and its
exactness check passes against the wrong Fvco:
if (div64_u64(fvco, divo) != frequency) {
The divider is committed, so the output runs at the real Fvco / N, while
out[].freq caches the requested rate. sit9531x_output_freq_get() divides
the same wrong Fvco, so userspace reads back the requested rate and no
error appears anywhere.
Using the out-of-band estimate also reports the result as exact, which is
the outcome the comment rejects for the band edge. Would it be safer to
refuse the set in this case?
Also, dev_warn_once() fires once per call site, not once per device or
PLL. After the first warning, other devices and PLLs are silent.
[ ... ]
> +static int sit9531x_output_phase_flush(struct sit9531x_dev *sitdev, u8 pll_idx)
> +{
[ ... ]
> + for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
> + if (i == pll_idx || !(armed_mask & BIT(i)))
> + continue;
> + rc = sit9531x_update_pll_u8(sitdev, i,
> + SIT9531X_PLL_REG_PHFL_CTRL,
> + SIT9531X_PLL_PHFL_ON_DEMAND_EN, 0);
> + if (rc)
> + goto unpark;
> + /* Cleared, if not latched yet: put back either way. */
> + parked |= BIT(i);
> + rc = sit9531x_pll_small_update(sitdev, i);
> + if (rc)
> + goto unpark;
> + }
> +
> + /* 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)
> + goto unpark;
[ ... ]
> + rc = sit9531x_read_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, &ctrl);
> + if (rc)
> + goto disarm;
> +
> + orig = ctrl & ~SIT9531X_DIVO_PHASE_TRIG;
> + ctrl = orig | SIT9531X_DIVO_PHASE_SEL_REG;
> + rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, ctrl);
> + if (rc)
> + goto disarm;
[Severity: Medium]
Elsewhere in this patch, in sit9531x_prg_enter() and in the
sit9531x_output_divo_write() rollback, a write that reports an error is
assumed to have possibly reached the part. Do the error paths here need
the same handling?
If this selector write fails but lands, goto disarm skips the restore of
orig. The chip-wide phase-trigger source then stays on the in-register
source. That is the hijacked trigger state the comment after the pulse
says must be avoided.
In the parking loop, parked |= BIT(i) is set only after
sit9531x_update_pll_u8() succeeds. If the clear on a sibling PLL fails but
lands, the unpark loop never re-arms that PLL.
If arming pll_idx fails, goto unpark skips disarm. An enable bit that
landed then stays staged on a PLL the profile left disarmed, and the next
small update on that PLL latches it.
[ ... ]
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index ebbf7f5a3394e..a576d918e68db 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -350,6 +350,29 @@ sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
[ ... ]
> +static int
> +sit9531x_dpll_input_pin_frequency_get(const struct dpll_pin *pin,
> + void *pin_priv,
> + const struct dpll_device *dpll,
> + void *dpll_priv, u64 *frequency,
> + struct netlink_ext_ack *extack)
> +{
> + struct sit9531x_dpll_pin *dpin = pin_priv;
> + struct sit9531x_dpll *sitdpll = dpll_priv;
> + const struct sit9531x_ref *ref;
> +
> + ref = sit9531x_ref_state_get(sitdpll->dev, dpin->id);
> + *frequency = ref->freq;
> +
> + return 0;
> +}
[Severity: Medium]
What does this report for an input that has no firmware node, or a node
without supported-frequencies-hz?
sitdev->ref[index].freq is only set in sit9531x_pin_props_get(). It comes
from freqs[0] when the node lists frequencies, or from xtal_freq for the
XO. Nothing else writes it.
sit9531x_input_pin_is_registrable() checks only the index and the DE
pairing, so inputs without frequency data are still registered.
For those pins ref->freq stays 0. Every pin-get, dump and notification
now reports a frequency of 0 Hz with no supported-frequency entries.
Before this patch the attribute was simply absent.
[ ... ]
> diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
> index 934db566c0020..42f3e53b6def2 100644
> --- a/drivers/dpll/sit9531x/prop.c
> +++ b/drivers/dpll/sit9531x/prop.c
> @@ -295,15 +295,35 @@ sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
> }
>
> /*
> - * Seed an input's runtime ref->freq with the first DT-listed
> - * supported frequency: for an input the board lists the rate that is
> - * physically wired to it first. An output's current rate is what its
> - * divider produces, not an entry of the list it may be set to.
> + * Seed the runtime ref->freq with the first DT-listed supported
> + * frequency: an input's rate is a board fact, so firmware is the
> + * source. An output is left to the read-back below, which knows
> + * what the divider is actually doing.
> */
> if (num_freqs > 0 && dir == DPLL_PIN_DIRECTION_INPUT &&
> index != SIT9531X_MAX_INPUTS)
> curr_freq = freqs[0];
[Severity: Medium]
With the new input frequency_get, userspace now sees this first entry as
the current input rate. Does the binding give the first entry that
meaning?
Documentation/devicetree/bindings/dpll/dpll-pin.yaml only says:
List of supported frequencies for this pin, expressed in Hz.
The SiTime binding does not give the first entry that meaning either.
A conformant DT can list several rates for a pin that may run at any of
them, such as <10000 10000000> in the zl30731 example. This driver would
then report 10000 Hz whatever rate the input is actually driven at.
Inputs have no frequency_set, so userspace cannot correct the reported
value.
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260930233714.87679-1-arouhi%40sitime.com
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH net-next v11 12/13] dpll: sit9531x: add support to get phase offset on the connected input pin
2026-09-30 23:37 [PATCH net-next v11 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (9 preceding siblings ...)
2026-09-30 23:37 ` [PATCH net-next v11 09/13] dpll: sit9531x: add support to get and set frequency on pins Ali Rouhi
@ 2026-09-30 23:37 ` Ali Rouhi
2026-10-05 1:10 ` netdev-bot+sashiko
2026-09-30 23:37 ` [PATCH net-next v11 11/13] dpll: sit9531x: add support to adjust output phase Ali Rouhi
2026-09-30 23:37 ` [PATCH net-next v11 13/13] dpll: sit9531x: model the inter-PLL sync net as a pair of pins Ali Rouhi
12 siblings, 1 reply; 26+ messages in thread
From: Ali Rouhi @ 2026-09-30 23:37 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi, devicetree,
netdev, linux-kernel
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Report the phase difference between a PLL's reference and the PLL itself,
which is the loop's own residual error and therefore trends small on a
locked device -- that is the measurement, not an artefact of it.
The value comes from the on-chip time-to-digital converter, read through
the debug window: unlock the window, point it at the converter, then read
the trigger register, which latches a fresh sample and returns the
previous one. It is read three times per sample for that reason; a single
read hands back the sample from the last call, so a repeated measurement
would look perfectly steady while saying nothing.
Only the input a PLL has actually selected has a phase offset against it.
For any other pin there is nothing to measure and zero is reported,
because the core abandons an entire pin dump on an error from any one pin.
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
drivers/dpll/sit9531x/core.c | 229 +++++++++++++++++++++++++++++++++++
drivers/dpll/sit9531x/core.h | 4 +
drivers/dpll/sit9531x/dpll.c | 130 ++++++++++++++++++++
drivers/dpll/sit9531x/regs.h | 35 ++++++
4 files changed, 398 insertions(+)
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index d2719ff24249..45baa703aa45 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -2760,6 +2760,235 @@ int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
return 0;
}
+/**
+ * sit9531x_chan_selected_ref_read - read a PLL's active reference now
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ * @ref: result, logical input index of the selected reference
+ *
+ * chan->selected_ref is refreshed by the monitor twice a second, which is
+ * close enough for reporting pin state but not for attributing a
+ * measurement: the device picks its own reference, so a sample taken now
+ * can belong to a pin the cache has not caught up with.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, <0 on error
+ */
+int sit9531x_chan_selected_ref_read(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 *ref)
+{
+ u8 activesel_reg, input_sel;
+ int rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+
+ 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;
+
+ input_sel &= SIT9531X_PRIO_NIBBLE_MASK;
+ input_sel = sit9531x_prio_src_canon(sitdev, input_sel);
+ *ref = sit9531x_hw_src_input(input_sel);
+
+ return 0;
+}
+
+/**
+ * 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) code from the PLL page
+ * registers -- a 35-bit magnitude with a separate sign bit -- then
+ * converts it to picoseconds using the VCO frequency:
+ * phase_diff = tdc_code / fvco.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, -ENODATA when the PLL has no known VCO rate
+ * (nothing is sampled then), or the register access error
+ */
+int sit9531x_phase_offset_read(struct sit9531x_dev *sitdev, u8 pll_idx,
+ s64 *phase_ps)
+{
+ u8 v, old_write_code, old_read_code;
+ bool have_old = false;
+ int rc, ret, i;
+ u64 fvco, mag_ps;
+ s64 tdc_signed;
+ u64 tdc_raw;
+ bool sign;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+
+ /*
+ * Get the VCO rate first. -ENODATA means the PLL has no rate to
+ * convert against (a divider this board leaves unprogrammed), and
+ * there is then no reason to open the debug window at all -- which
+ * also keeps a bus error while it is open from ever being reported
+ * as that benign case.
+ */
+ rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
+ if (rc) {
+ if (rc == -ENODATA)
+ dev_dbg(sitdev->dev,
+ "PLL%c: Fvco unknown, skip TDC\n",
+ 'A' + pll_idx);
+ return rc;
+ }
+
+ /* Unlock the debug page so the TDC registers are accessible. */
+ rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DEBUG,
+ SIT9531X_PLL_DEBUG_UNLOCK);
+ if (rc)
+ goto relock;
+
+ /*
+ * Remember the tap selection so it can be put back. The key
+ * register is re-locked below, but the mux is not part of the key:
+ * leaving it parked on the TDC with a slow sampling clock selected
+ * is a state change the caller did not ask for, and the next reader
+ * of a different tap would have to know to undo it.
+ */
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_WRITE_CODE,
+ &old_write_code);
+ if (!rc)
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_READ_CODE,
+ &old_read_code);
+ if (rc)
+ goto relock;
+
+ have_old = true;
+
+ /*
+ * Select the debug clock for taps below 200 kHz, then point the
+ * readback at the TDC. Only the one bit is touched: writing the
+ * modifier register whole would clear the fields belonging to
+ * other taps.
+ */
+ rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_WRITE_CODE,
+ SIT9531X_DBG_LOW_FREQ_CLK_BIT,
+ SIT9531X_DBG_LOW_FREQ_CLK_BIT);
+ if (rc)
+ goto relock;
+ rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_READ_CODE,
+ SIT9531X_DBG_READ_CODE_TDC);
+ if (rc)
+ goto relock;
+
+ /*
+ * Latch a sample by reading the trigger register. A single
+ * read returns the previous latch, so read it three times as
+ * the documented phase-difference procedure does.
+ */
+ for (i = 0; i < SIT9531X_DBG_LATCH_READS; i++) {
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_TRIGGER, &v);
+ if (rc)
+ goto relock;
+ }
+
+ tdc_raw = 0;
+
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_DATA_4, &v);
+ if (rc)
+ goto relock;
+ sign = !!(v & BIT(SIT9531X_TDC_SIGN_BIT));
+ tdc_raw = (u64)(v & SIT9531X_TDC_MAG_HI_MASK) << 32;
+
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_DATA_3, &v);
+ if (rc)
+ goto relock;
+ tdc_raw |= (u64)v << 24;
+
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_DATA_2, &v);
+ if (rc)
+ goto relock;
+ tdc_raw |= (u64)v << 16;
+
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_DATA_1, &v);
+ if (rc)
+ goto relock;
+ tdc_raw |= (u64)v << 8;
+
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_DATA_0, &v);
+ if (rc)
+ goto relock;
+ tdc_raw |= v;
+
+ /*
+ * Apply sign. Per the register map the sign bit is active-high
+ * for a positive offset: bit set -> +code, bit clear -> -code.
+ */
+ tdc_signed = sign ? (s64)tdc_raw : -(s64)tdc_raw;
+
+ /*
+ * phase_diff (seconds) = tdc_code / fvco
+ * phase_diff (ps) = tdc_code * 1e12 / fvco
+ *
+ * mul_u64_u64_div_u64() keeps the exact Hz denominator; dividing
+ * by whole MHz instead would lose up to ~40 ppm of scale on a
+ * fractional-DIVN Fvco.
+ */
+ mag_ps = mul_u64_u64_div_u64(tdc_signed < 0 ? -tdc_signed : tdc_signed,
+ 1000000000000ULL, fvco);
+ *phase_ps = tdc_signed < 0 ? -(s64)mag_ps : (s64)mag_ps;
+
+ rc = 0;
+
+relock:
+ if (have_old) {
+ ret = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_READ_CODE,
+ old_read_code);
+ if (ret && !rc)
+ rc = ret;
+ ret = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_WRITE_CODE,
+ old_write_code);
+ if (ret && !rc)
+ rc = ret;
+ }
+
+ /*
+ * Close the debug window again. The key register opens every debug
+ * register on this PLL while it holds the unlock value, and this read
+ * runs on every pin-get of a connected input, so leaving it open
+ * would mean normal monitoring permanently unlocks the block.
+ */
+ ret = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DEBUG,
+ SIT9531X_PLL_DEBUG_LOCK);
+ if (ret && !rc)
+ rc = ret;
+
+ return rc;
+}
+
/*
* sit9531x_ref_state_fetch - read input reference status from hardware
* @index: logical input index
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 66c87e8f0387..bebdc9f5abce 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -296,6 +296,10 @@ int sit9531x_clear_notifications(struct sit9531x_dev *sitdev);
/* ---- INTSYNC (inter-PLL synchronization) ---- */
/* ---- Phase offset (TDC readback) ---- */
+int sit9531x_chan_selected_ref_read(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 *ref);
+int sit9531x_phase_offset_read(struct sit9531x_dev *sitdev, u8 pll_idx,
+ s64 *phase_ps);
/* ---- State helpers ---- */
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index 8a5e3a1decd0..6bf4efd3c633 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -605,6 +605,135 @@ sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
return 0;
}
+/*
+ * sit9531x_dpll_input_pin_phase_offset_get - phase offset of a reference
+ *
+ * What this reports, and what it deliberately does not:
+ *
+ * The ABI defines the attribute as the phase difference between the signal
+ * on a pin and its parent DPLL device, so this is the loop's own residual
+ * error, sampled with the loop closed. On a locked DPLL it therefore
+ * trends small -- that is the measurement, not an artefact of it. The
+ * documentation describes the reported value as one that may be averaged
+ * over prior measurements, which suits a closed-loop residual and not a
+ * one-shot open-loop capture; the core publishes whatever this callback
+ * returns, so the averaging, if any, would be this driver's to do.
+ *
+ * The chip can also measure the reference against the local oscillator
+ * with the outer loop's correction frozen, which is a different quantity
+ * and the one the documented phase-difference procedure produces. That
+ * needs the digital loop filter held (and, on the 1PPS PLL, the automatic
+ * phase- and frequency-lock helpers held off), which leaves the PLL
+ * undisciplined until it is released. A netlink read must not do that,
+ * so that measurement is not offered here at all; it belongs to a caller
+ * that can own the freeze and restore it.
+ *
+ * Precondition, which this callback cannot create: the TDC compares
+ * against a signal the PLL drives, so a PLL driving no output with its
+ * zero-delay buffer off has nothing to measure. SiTime clock
+ * engineering confirms this is a property of the hardware, not of
+ * SiTime's TDC measurement procedure, which satisfies it by mapping a
+ * spare output and restarting the PLL -- side effects that do not belong
+ * in a getter, so a reading taken in that state is simply not meaningful.
+ *
+ * Non-selected pins and a PLL with no programmed divider report zero
+ * rather than an error: the DPLL core propagates any error from this
+ * callback and fails the whole pin dump with it. The core has no per-pin
+ * "no data" for phase offset, as it has -ENODATA for the fractional
+ * frequency offset, so it is a value or no callback at all.
+ */
+static int
+sit9531x_dpll_input_pin_phase_offset_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, s64 *phase_offset,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ bool drives = false;
+ s64 offset;
+ u8 selected, i;
+ 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,
+ * so an input that is not the active reference reports 0 rather
+ * than the active reference's value.
+ *
+ * The sample needs both: the PLL tracking this pin as the poll last
+ * saw it (locked, outer loop running, not frozen), and the device
+ * still naming this pin, read now -- the device selects on its own,
+ * and a cache up to a poll period old could attribute a live
+ * measurement to the pin that used to be selected. The device is
+ * read again after the sample for the same reason: it can switch
+ * during the dozen transfers the sample takes.
+ *
+ * The TDC also compares against a signal the PLL drives, so a PLL
+ * with no routed, driving output has nothing to measure and reports
+ * 0 like any other pin without a reading.
+ */
+ for (i = 0; i < sitdev->info->num_outputs; i++)
+ if (sitdev->out[i].routed && sitdev->out[i].enabled &&
+ sitdev->out[i].pll_idx == sitdpll->id)
+ drives = true;
+
+ if (!drives ||
+ !sit9531x_dpll_selection_active(sitdev, sitdpll, dpin->id)) {
+ mutex_unlock(&sitdev->multiop_lock);
+ *phase_offset = 0;
+ return 0;
+ }
+
+ rc = sit9531x_chan_selected_ref_read(sitdev, sitdpll->id,
+ &selected);
+ if (!rc && selected == dpin->id) {
+ rc = sit9531x_phase_offset_read(sitdev, sitdpll->id, &offset);
+ if (!rc)
+ rc = sit9531x_chan_selected_ref_read(sitdev,
+ sitdpll->id,
+ &selected);
+ if (!rc && selected != dpin->id)
+ rc = -ENODATA;
+ } else if (!rc) {
+ rc = -ENODATA;
+ }
+ mutex_unlock(&sitdev->multiop_lock);
+
+ /*
+ * -ENODATA means no reading: the PLL has no known VCO rate, or the
+ * selection moved off this pin around the sample. Report 0 so a
+ * full pin-get dump does not fail over it. Every other errno,
+ * -ENODEV from a vanished adapter included, is a failure.
+ */
+ if (rc == -ENODATA) {
+ *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;
+
+ *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,
@@ -613,6 +742,7 @@ static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
.operstate_on_dpll_get = sit9531x_dpll_input_pin_operstate_on_dpll_get,
.prio_get = sit9531x_dpll_input_pin_prio_get,
.prio_set = sit9531x_dpll_input_pin_prio_set,
+ .phase_offset_get = sit9531x_dpll_input_pin_phase_offset_get,
};
/*
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index b805cbc41fc1..0ba3c73aa3ed 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -283,6 +283,41 @@
#define SIT9531X_PLL_REG_DIVN_NUM 0x32 /* 4 bytes (0x32-0x35) */
#define SIT9531X_PLL_REG_DIVN_DEN 0x38 /* 4 bytes (0x38-0x3B) */
+/* Debug register unlock */
+#define SIT9531X_PLL_REG_DEBUG 0xBD
+#define SIT9531X_PLL_DEBUG_UNLOCK 0xC3
+#define SIT9531X_PLL_DEBUG_LOCK 0x00
+
+/*
+ * Signal pathway debug readback -- PLL page. Dig_Sys_ReadCode selects
+ * which point of the pathway is tapped, Dig_Sys_WriteCode carries the
+ * modifiers for that read, Dig_Sys_read7..read0 hold the sampled bytes
+ * and the trigger register latches a sample. The TDC phase
+ * measurement is one tap among several, reached through read code 69.
+ */
+#define SIT9531X_PLL_REG_DBG_READ_CODE 0xB3
+#define SIT9531X_PLL_REG_DBG_WRITE_CODE 0xB4
+#define SIT9531X_DBG_LOW_FREQ_CLK_BIT BIT(7)
+#define SIT9531X_PLL_REG_DBG_DATA_0 0xB5 /* [7:0] */
+#define SIT9531X_PLL_REG_DBG_DATA_1 0xB6 /* [15:8] */
+#define SIT9531X_PLL_REG_DBG_DATA_2 0xB7 /* [23:16] */
+#define SIT9531X_PLL_REG_DBG_DATA_3 0xB8 /* [31:24] */
+#define SIT9531X_PLL_REG_DBG_DATA_4 0xB9 /* [34:32] + sign */
+/* read to latch a sample */
+#define SIT9531X_PLL_REG_DBG_TRIGGER 0xD0
+
+/*
+ * Reads of the trigger needed to latch a fresh sample. One returns the
+ * previous latch, which the documented procedures work around by reading it
+ * three times.
+ */
+#define SIT9531X_DBG_LATCH_READS 3
+
+/* Read code of the TDC phase tap, and the sign bit of its sample */
+#define SIT9531X_DBG_READ_CODE_TDC 69
+#define SIT9531X_TDC_SIGN_BIT 3
+#define SIT9531X_TDC_MAG_HI_MASK GENMASK(2, 0)
+
/* DIVN is carried as fixed point, in steps of 1e-12 of a whole divider */
#define SIT9531X_DIVN_SCALE 1000000000000ULL
--
2.43.0
^ permalink raw reply [flat|nested] 26+ messages in thread* Re: [PATCH net-next v11 12/13] dpll: sit9531x: add support to get phase offset on the connected input pin
2026-09-30 23:37 ` [PATCH net-next v11 12/13] dpll: sit9531x: add support to get phase offset on the connected input pin Ali Rouhi
@ 2026-10-05 1:10 ` netdev-bot+sashiko
0 siblings, 0 replies; 26+ messages in thread
From: netdev-bot+sashiko @ 2026-10-05 1:10 UTC (permalink / raw)
To: arouhi
Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba,
pabeni, robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
devicetree, netdev, linux-kernel
Thank you for your contribution! Sashiko AI review found 5 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 3 · Low: 2
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] sit9531x_phase_offset_read() (core.c:2957) computes
mul_u64_u64_div_u64(|tdc|, 1e12, fvco) with no limit on the quotient,…
- [Medium] sit9531x_dpll_input_pin_phase_offset_get() (dpll.c:681-684)
decides whether the PLL drives an output, which the TDC needs, by…
- [Medium] The live checks in sit9531x_dpll_input_pin_phase_offset_get()
(dpll.c:693 and 698) are meant to stop a TDC sample being credited to…
- [Low] In drivers/dpll/sit9531x/dpll.c,
sit9531x_dpll_input_pin_phase_offset_get() sets *phase_offset = 0 in…
- [Low] sit9531x_phase_offset_read() in drivers/dpll/sit9531x/core.c
(line 2957) converts the TDC code to whole picoseconds with integer…
--
dpll: sit9531x: add support to get phase offset on the connected input pin
This adds a phase_offset_get callback for sit9531x input pins. It reads
the on-chip TDC through the PLL debug window and converts the code to
picoseconds using the VCO rate. Only the input a PLL currently has
selected reports a measurement, and other pins report zero.
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index d2719ff24249f..45baa703aa45b 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -2760,6 +2760,235 @@ int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
[ ... ]
> +int sit9531x_phase_offset_read(struct sit9531x_dev *sitdev, u8 pll_idx,
> + s64 *phase_ps)
> +{
[ ... ]
> + rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
> + if (rc) {
> + if (rc == -ENODATA)
> + dev_dbg(sitdev->dev,
> + "PLL%c: Fvco unknown, skip TDC\n",
> + 'A' + pll_idx);
> + return rc;
> + }
[ ... ]
> + /*
> + * phase_diff (seconds) = tdc_code / fvco
> + * phase_diff (ps) = tdc_code * 1e12 / fvco
> + *
> + * mul_u64_u64_div_u64() keeps the exact Hz denominator; dividing
> + * by whole MHz instead would lose up to ~40 ppm of scale on a
> + * fractional-DIVN Fvco.
> + */
> + mag_ps = mul_u64_u64_div_u64(tdc_signed < 0 ? -tdc_signed : tdc_signed,
> + 1000000000000ULL, fvco);
> + *phase_ps = tdc_signed < 0 ? -(s64)mag_ps : (s64)mag_ps;
[Severity: Medium]
What limits the size of the quotient here?
tdc_raw comes from DBG_DATA_4..0 and can be as large as 2^35 - 1. The
only requirement on fvco is that it is non-zero. For an out-of-band
rate, sit9531x_get_fvco() warns once and still returns success:
if (*fvco < fvco_min || *fvco > fvco_max)
dev_warn_once(sitdev->dev, ...);
return 0;
Also, sit9531x_divn_static() accepts int_part = 0 with any |NUM| < DEN,
so DIVN can be far below 1.
With a 48 MHz XO and DIVN below about 4e-5, fvco falls under about
1.86 kHz. At that point |tdc| * 1e12 / fvco no longer fits in u64.
On x86_64, mul_u64_u64_div_u64() is an inline mulq/divq. Its comment in
arch/x86/include/asm/div64.h says it "Will generate an #DE when the
result doesn't fit u64". Could a DPLL_CMD_PIN_GET or a pin notification
then oops with a divide error?
Less extreme rates also give wrong values. Below about 3.7 kHz, the
(s64) cast flips the sign. Below about 3.7 MHz, this line in
sit9531x_dpll_input_pin_phase_offset_get() overflows s64:
offset *= DPLL_PHASE_OFFSET_DIVIDER;
This needs DIVN registers that read as a tiny fraction on a PLL that also
reports lock, so it may not happen in practice. Still,
sit9531x_divn_static() already guards the same x86 case for the NUM/DEN
pair ("a divide-error exception on x86, not a value a caller could
reject").
Should the TDC conversion get a similar bound, for example by rejecting
an fvco outside the VCO band? Later patches in the series don't appear to
change this code.
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index 8a5e3a1decd06..6bf4efd3c6334 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -605,6 +605,135 @@ sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
[ ... ]
> +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)
> +{
[ ... ]
> + for (i = 0; i < sitdev->info->num_outputs; i++)
> + if (sitdev->out[i].routed && sitdev->out[i].enabled &&
> + sitdev->out[i].pll_idx == sitdpll->id)
> + drives = true;
[Severity: Medium]
Should this loop check sitdev->out[i].state_stale?
When the sit9531x_output_forced_hiz() readback fails after the commit,
sit9531x_output_enable() and sit9531x_output_disable() set state_stale
and keep the old enabled value. The Hi-Z writes may still have taken
effect:
/*
* The writes may well have landed; what failed is the proof.
* Mark the cached state for a read-through rather than
* reporting the value it had before this call.
*/
sitdev->out[index].state_stale = true;
sit9531x_dpll_output_pin_state_on_dpll_get() handles this by calling
sit9531x_output_state_refresh() first, because output state is never
polled.
Say a disable of this PLL's last enabled output takes effect but its
readback fails. Then drives stays true, and a sample that the callback
comment calls not meaningful gets published.
In the matching enable case, drives stays false and real readings are
reported as 0. Since nothing polls output state, either case lasts until
something else refreshes that output.
Could stale outputs be refreshed here too, the way the output pin getter
does it?
> +
> + if (!drives ||
> + !sit9531x_dpll_selection_active(sitdev, sitdpll, dpin->id)) {
> + mutex_unlock(&sitdev->multiop_lock);
> + *phase_offset = 0;
> + return 0;
> + }
> +
> + rc = sit9531x_chan_selected_ref_read(sitdev, sitdpll->id,
> + &selected);
> + if (!rc && selected == dpin->id) {
> + rc = sit9531x_phase_offset_read(sitdev, sitdpll->id, &offset);
> + if (!rc)
> + rc = sit9531x_chan_selected_ref_read(sitdev,
> + sitdpll->id,
> + &selected);
> + if (!rc && selected != dpin->id)
> + rc = -ENODATA;
> + } else if (!rc) {
> + rc = -ENODATA;
> + }
[Severity: Medium]
Do these two live reads catch the case they are meant to catch?
Both reads only look at CLK_ACTIVESEL_PLL, through
sit9531x_chan_selected_ref_read(). Lock, outer loop, holdover freeze and
LOS all come from the poll cache checked in
sit9531x_dpll_selection_active(), and that cache can be up to one poll
period old.
The comment in sit9531x_dpll_selection_active() says CLK_ACTIVESEL does
not show the device's own fallback away from a lane with no signal:
/*
* A selection naming a lane without signal is not what the PLL runs
* on: the device has fallen back to another listed source on its
* own, and this driver does not read which. ...
*/
Suppose the selected input loses signal after the last poll, or the PLL
loses lock or enters holdover. Wouldn't both reads still return dpin->id?
If so, the residual against another source, or the holdover drift, would
be published as this pin's phase offset.
The callback comment says the live read exists because "a cache up to a
poll period old could attribute a live measurement to the pin that used
to be selected". Should lock, holdover and LOS for this pin also be read
live around the sample? Later patches in the series don't appear to
change this code.
> + mutex_unlock(&sitdev->multiop_lock);
> +
> + /*
> + * -ENODATA means no reading: the PLL has no known VCO rate, or the
> + * selection moved off this pin around the sample. Report 0 so a
> + * full pin-get dump does not fail over it. Every other errno,
> + * -ENODEV from a vanished adapter included, is a failure.
> + */
> + if (rc == -ENODATA) {
> + *phase_offset = 0;
> + return 0;
> + }
[Severity: Low]
The commit message says:
Only the input a PLL has actually selected has a phase offset against it.
For any other pin there is nothing to measure and zero is reported,
because the core abandons an entire pin dump on an error from any one pin.
Here and in the early return above, the selected pin can also report 0:
- sit9531x_dpll_selection_active() sees the PLL unlocked, with the
outer loop off, or frozen
- no routed and enabled output belongs to this PLL
- sit9531x_get_fvco() returns -ENODATA
- the selection changes during the sample
The stated reason holds. dpll_msg_add_phase_offset() returns any callback
error directly, and only dpll_msg_add_ffo() treats -ENODATA as "omit".
zl3073x also reports 0 when the DPLL is not locked to the reference, and
the callback comments here list each case.
Could the commit message also say that the connected pin reads 0 when no
sample was taken? That way a 0 on that pin isn't taken as a measured
value.
[ ... ]
> + /*
> + * 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;
[Severity: Low]
Is the reason given in this comment accurate? One TDC code is
1e12 / fvco ps, which is usually not a whole number. A 4.9152 GHz VCO
gives about 203.45 ps per code, and a fractional-DIVN Fvco is usually
not a round value either.
The fractional digits are always zero because sit9531x_phase_offset_read()
truncates to whole picoseconds before this multiply:
mag_ps = mul_u64_u64_div_u64(tdc_signed < 0 ? -tdc_signed : tdc_signed,
1000000000000ULL, fvco);
Documentation/driver-api/dpll.rst defines a 3-digit fractional part for
this value. Could the conversion compute |tdc| * 1e15 / fvco, with an
overflow bound, so the fraction is kept? If not, could the comment be
corrected?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260930233714.87679-1-arouhi%40sitime.com
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH net-next v11 11/13] dpll: sit9531x: add support to adjust output phase
2026-09-30 23:37 [PATCH net-next v11 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (10 preceding siblings ...)
2026-09-30 23:37 ` [PATCH net-next v11 12/13] dpll: sit9531x: add support to get phase offset on the connected input pin Ali Rouhi
@ 2026-09-30 23:37 ` Ali Rouhi
2026-10-05 1:10 ` netdev-bot+sashiko
2026-09-30 23:37 ` [PATCH net-next v11 13/13] dpll: sit9531x: model the inter-PLL sync net as a pair of pins Ali Rouhi
12 siblings, 1 reply; 26+ messages in thread
From: Ali Rouhi @ 2026-09-30 23:37 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi, devicetree,
netdev, linux-kernel
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Shift an output in time against the others driven by the same PLL. The
device has a coarse delay counted in VCO cycles and a three-bit fine field
in fixed thirty-picosecond steps, so a requested offset is split between
the two and what the core reads back is what the registers hold rather
than what was asked for. The divider spends two VCO cycles acting on a
programmed delay before it releases the output, so the registers hold the
request plus those two cycles and the read-back takes them off again.
The window advertised to the core is one millisecond either way, which is
wider than the dynamic range but costs nothing and is what keeps the
subsystem from refusing every request; the granularity is one picosecond,
because the achievable delays are whole VCO cycles plus thirty-picosecond
steps and so form no uniform lattice for the core to check against.
Delay only ever advances, so an offset larger than one output period is
folded back into a single period -- for a periodic signal that is the same
phase. The write takes effect in the programming state, which is left
with the loops re-locked even when a write inside it failed.
The device has no per-output phase flush, so realigning the adjusted
output restarts the divider phase of every output that PLL drives. On a
part where outputs are deliberately skewed against each other that is a
visible edge jump on the others, and there is no register that would let
the driver avoid it.
Suggested-by: Ivan Vecera <ivecera@redhat.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>
---
drivers/dpll/sit9531x/core.c | 435 ++++++++++++++++++++++++++++++++++-
drivers/dpll/sit9531x/core.h | 16 ++
drivers/dpll/sit9531x/dpll.c | 78 +++++++
drivers/dpll/sit9531x/prop.c | 20 ++
drivers/dpll/sit9531x/regs.h | 36 +++
5 files changed, 579 insertions(+), 6 deletions(-)
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 8fed14d8dca6..d2719ff24249 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -2183,6 +2183,110 @@ static int sit9531x_output_divo_read(struct sit9531x_dev *sitdev, u8 out_idx,
return *divo ? 0 : -ENODATA;
}
+/**
+ * sit9531x_output_phase_read - read an output's programmed delay back
+ * @sitdev: device pointer
+ * @out_idx: logical output index
+ * @phase_ps: result in picoseconds, in the advertised range
+ *
+ * The delay the chip holds is part of the profile it loads before probe,
+ * and a rate or phase request that failed after its writes reached the
+ * device leaves the cache describing something else. Decoding the five
+ * PRG_RST_DELAY bytes is the only way to say what the output is really
+ * doing. The registers carry an unsigned delay; one beyond the
+ * advertised range whose complement to the output period is within it is
+ * how an advance is held, and reads back as that advance.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, -ENODATA for an output no PLL drives or whose VCO
+ * rate is unknown, <0 on register access error
+ */
+int sit9531x_output_phase_read(struct sit9531x_dev *sitdev, u8 out_idx,
+ s32 *phase_ps)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ u8 bytes[5], page, base, slot, fine, i;
+ u64 coarse = 0, fvco, ps, divo;
+ int rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (out_idx >= info->num_outputs)
+ return -EINVAL;
+
+ /*
+ * An output no PLL drives has no Fvco to decode its delay against;
+ * pll_idx then holds a placeholder, not a routing.
+ */
+ if (!sitdev->out[out_idx].routed)
+ return -ENODATA;
+
+ rc = sit9531x_get_fvco(sitdev, sitdev->out[out_idx].pll_idx, &fvco);
+ if (rc)
+ return rc;
+
+ 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);
+
+ for (i = 0; i < ARRAY_SIZE(bytes); i++) {
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG(page, base + i),
+ &bytes[i]);
+ if (rc)
+ return rc;
+ }
+
+ fine = (bytes[0] & SIT9531X_OUT_PRG_FINE_MASK) >>
+ SIT9531X_OUT_PRG_FINE_SHIFT;
+ coarse = (u64)(bytes[0] & SIT9531X_OUT_PRG_COARSE_HI_MASK) << 32;
+ coarse |= (u64)bytes[1] << 24;
+ coarse |= (u64)bytes[2] << 16;
+ coarse |= (u64)bytes[3] << 8;
+ coarse |= bytes[4];
+
+ /*
+ * The register carries the divider's settling time on top of the
+ * delay that was asked for, so take it back off. A profile can
+ * leave a value below it, which describes no delay at all.
+ */
+ coarse = (coarse > SIT9531X_OUT_PRG_DIVO_CYCLES) ?
+ coarse - SIT9531X_OUT_PRG_DIVO_CYCLES : 0;
+
+ ps = mul_u64_u64_div_u64(coarse, 1000000000000ULL, fvco);
+ ps += (u64)fine * SIT9531X_OUT_PRG_FINE_STEP_PS;
+
+ /*
+ * The setter holds an advance as T_out - |advance|, so a delay
+ * beyond the advertised range whose complement is within it is that
+ * advance and reads back as one. Anything else a profile left
+ * beyond the range -- wider than an s32 on a slow output -- reports
+ * the end of the range rather than a value the setter would refuse.
+ *
+ * The period comes from the divider rather than from the cached rate,
+ * which is still unset at probe and whole hertz at best. An output
+ * without a programmed divider has no period to fold against.
+ */
+ if (!sit9531x_output_divo_read(sitdev, out_idx, &divo)) {
+ u64 t_out_ps = mul_u64_u64_div_u64(divo, 1000000000000ULL,
+ fvco);
+
+ if (t_out_ps) {
+ div64_u64_rem(ps, t_out_ps, &ps);
+ if (ps > SIT9531X_OUT_PHASE_ADJ_MAX_PS &&
+ t_out_ps - ps <= SIT9531X_OUT_PHASE_ADJ_MAX_PS) {
+ *phase_ps = -(s32)(t_out_ps - ps);
+ return 0;
+ }
+ }
+ }
+ *phase_ps = (s32)min_t(u64, ps, SIT9531X_OUT_PHASE_ADJ_MAX_PS);
+
+ return 0;
+}
+
int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
u8 pll_idx, u64 frequency)
{
@@ -2236,7 +2340,38 @@ int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
sitdev->out[out_idx].freq = div64_u64(fvco, divo);
- return 0;
+ /*
+ * The programmed reset delay counts VCO cycles, and a rate change
+ * moves only the divider, so a positive delay keeps its timing; an
+ * advance, though, is held as T_out - |advance| and has to be
+ * re-encoded against the new period. Re-encode whatever was asked
+ * for: for a positive delay that lands on the same register bytes,
+ * and sit9531x_output_phase_adjust_set() then writes nothing.
+ */
+ if (sitdev->out[out_idx].phase_armed) {
+ s32 phase_ps = sitdev->out[out_idx].phase_adj;
+ int ph_rc;
+
+ /*
+ * The rate is already programmed and latched at this point.
+ * Failing the request for a re-timing that did not take
+ * would report a frequency set that did not happen, and the
+ * core drops an identical retry because it asks the driver
+ * for the current rate first -- which is the new one. Say
+ * what went wrong and mark the delay for a read-back
+ * instead.
+ */
+ ph_rc = sit9531x_output_phase_adjust_set(sitdev, out_idx,
+ phase_ps);
+ if (ph_rc) {
+ sitdev->out[out_idx].phase_stale = true;
+ dev_warn(sitdev->dev,
+ "out%u: rate changed but the phase adjust was not re-timed (%d)\n",
+ out_idx, ph_rc);
+ }
+ }
+
+ return rc;
}
/*
@@ -2304,14 +2439,277 @@ int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
* base + 3 PROG3 PRG_RST_DELAY[15:8]
* base + 4 PROG2 PRG_RST_DELAY[7:0]
*
- * Outputs 0-5 live on Page 3, outputs 6-11 on Page 4, with each
- * output's block at base = 0x15 + 16 * (out_idx % 6).
+ * Slots 0-5 live on Page 3, slots 6-11 on Page 4, with each slot's
+ * block at base = 0x15 + 16 * (slot % 6); the slot is the physical
+ * output position from clkout_map[], not the logical output index.
*
- * The chip only supports unsigned positive delay. A negative phase
- * adjustment (advance) is wrapped to (T_out - |phase|) modulo one
- * output period, which is identical for a periodic signal.
+ * The chip only supports unsigned positive delay. Requests are folded
+ * modulo one output period: positive delays wrap naturally and a negative
+ * phase adjustment (advance) is rendered as (T_out - |phase|).
*/
+int sit9531x_output_phase_adjust_set(struct sit9531x_dev *sitdev,
+ u8 out_idx, s32 phase_ps)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ u64 abs_ps, fvco, coarse = 0, coarse_ps, t_out_ps, prg_coarse;
+ s64 phase_norm_ps = 0;
+ u8 page, base, prog6_val, fine = 0;
+ u8 old_bytes[5], new_bytes[5], i;
+ u8 pll_idx, slot;
+ u64 divo;
+ int rc, ret, rb_rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (out_idx >= info->num_outputs)
+ return -EINVAL;
+
+ pll_idx = sitdev->out[out_idx].pll_idx;
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+
+ rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
+ if (rc)
+ return rc == -ENODATA ? -ENODEV : rc;
+
+ /*
+ * The output period comes from the divider the output runs on, not
+ * from the cached rate: that is whole hertz, so a profile's output at
+ * a fractional rate would fold an advance against the wrong period.
+ */
+ rc = sit9531x_output_divo_read(sitdev, out_idx, &divo);
+ if (rc)
+ return rc == -ENODATA ? -ENODEV : rc;
+
+ t_out_ps = mul_u64_u64_div_u64(divo, 1000000000000ULL, fvco);
+ if (!t_out_ps)
+ return -EINVAL;
+
+ /*
+ * Convert to unsigned absolute delay. Both signs are folded
+ * modulo one period: positive delays wrap naturally, negative
+ * delays are rendered as T_out - |phase|. abs() is safe here
+ * because the core rejects anything outside the advertised phase
+ * range, which is +/-1 ms. div64_u64_rem() rather than the %
+ * operator: a 64-bit modulo has no compiler helper on 32-bit
+ * targets and leaves the module with an undefined __umoddi3.
+ */
+ abs_ps = abs(phase_ps);
+ div64_u64_rem(abs_ps, t_out_ps, &abs_ps);
+ phase_norm_ps = phase_ps < 0 ? -(s64)abs_ps : (s64)abs_ps;
+ abs_ps = (phase_ps < 0 && abs_ps) ? t_out_ps - abs_ps : abs_ps;
+
+ if (abs_ps) {
+ u64 rem_ps, err, up_ps;
+
+ /*
+ * 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);
+
+ /*
+ * Fine = round((abs_ps - coarse * vco_period_ps) / 30 ps).
+ * The fine field tops out below one VCO period on a slow VCO,
+ * so one more coarse cycle can land closer than the capped
+ * fine field; take whichever of the two is nearer.
+ */
+ 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;
+ }
+ err = abs_diff(abs_ps, coarse_ps +
+ (u64)fine * SIT9531X_OUT_PRG_FINE_STEP_PS);
+ up_ps = mul_u64_u64_div_u64(coarse + 1, 1000000000000ULL,
+ fvco);
+ if (up_ps - abs_ps < err) {
+ coarse++;
+ fine = 0;
+ }
+
+ /*
+ * A delay that quantizes to a whole output period or beyond
+ * is the same edge as no delay at all; program none, so the
+ * registers hold no residual past the period and the cache
+ * below describes exactly what they realize.
+ */
+ coarse_ps = mul_u64_u64_div_u64(coarse, 1000000000000ULL, fvco);
+ if (coarse_ps + (u64)fine * SIT9531X_OUT_PRG_FINE_STEP_PS >=
+ t_out_ps) {
+ coarse = 0;
+ fine = 0;
+ }
+
+ if (coarse + SIT9531X_OUT_PRG_DIVO_CYCLES >=
+ (1ULL << SIT9531X_OUT_PRG_COARSE_BITS))
+ return -ERANGE;
+ }
+
+ /*
+ * 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);
+
+ for (i = 0; i < ARRAY_SIZE(old_bytes); i++) {
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG(page, base + i),
+ &old_bytes[i]);
+ if (rc)
+ return rc;
+ }
+
+ /*
+ * Carry the divider's own settling time. Everything above works in
+ * the delay the caller asked for; the register wants that plus the
+ * two VCO cycles the divider spends acting on it, and a request of
+ * zero still waits those two. Only the register value carries them:
+ * coarse stays the requested delay, which is what gets cached.
+ */
+ prg_coarse = coarse + SIT9531X_OUT_PRG_DIVO_CYCLES;
+
+ /* PROG6 RMW: preserve OPSTG_VCASC_BUMP in [7:5] */
+ prog6_val = old_bytes[0] & SIT9531X_OUT_PRG_OPSTG_MASK;
+ prog6_val |= (fine << SIT9531X_OUT_PRG_FINE_SHIFT) &
+ SIT9531X_OUT_PRG_FINE_MASK;
+ prog6_val |= (u8)((prg_coarse >> 32) & SIT9531X_OUT_PRG_COARSE_HI_MASK);
+
+ new_bytes[0] = prog6_val;
+ new_bytes[1] = (u8)((prg_coarse >> 24) & 0xFF);
+ new_bytes[2] = (u8)((prg_coarse >> 16) & 0xFF);
+ new_bytes[3] = (u8)((prg_coarse >> 8) & 0xFF);
+ new_bytes[4] = (u8)(prg_coarse & 0xFF);
+
+ /*
+ * The pin advertises 1 ps granularity but caches the quantized
+ * value, so a repeated off-grid request reaches here with the
+ * registers already holding it. Rewriting them would still restart
+ * the divider phase of every output on the PLL; skip it unless an
+ * earlier failure left the delay unconfirmed.
+ */
+ if (!memcmp(old_bytes, new_bytes, sizeof(new_bytes)) &&
+ !sitdev->out[out_idx].phase_stale)
+ goto cache;
+
+ /*
+ * The PRG_RST_DELAY bytes live in the output system, so the writes
+ * only take effect when made inside the PRG_CMD programming state and
+ * committed to the NVM shadow, exactly like sit9531x_output_freq_set().
+ */
+ rc = sit9531x_prg_enter(sitdev);
+ if (rc)
+ return rc;
+
+ for (i = 0; i < ARRAY_SIZE(new_bytes); i++) {
+ rc = sit9531x_write_u8(sitdev,
+ SIT9531X_REG(page, base + i),
+ new_bytes[i]);
+ if (rc)
+ goto rollback;
+ }
+
+ goto commit;
+
+rollback:
+ rb_rc = 0;
+ for (i = 0; i < ARRAY_SIZE(old_bytes); i++) {
+ ret = sit9531x_write_u8(sitdev,
+ SIT9531X_REG(page, base + i),
+ old_bytes[i]);
+ if (ret && !rb_rc)
+ rb_rc = ret;
+ }
+ if (rb_rc) {
+ dev_err(sitdev->dev,
+ "out%u: phase-adjust rollback failed (%d), the delay registers are part old and part new\n",
+ out_idx, rb_rc);
+ if (!rc)
+ rc = rb_rc;
+ }
+
+commit:
+ /*
+ * Always leave the PRG_CMD state via prg_commit(), even on a
+ * mid-sequence write failure, so the output loops are re-locked rather
+ * than stranded unlocked; keep the first error.
+ */
+ ret = sit9531x_prg_commit(sitdev);
+ if (ret && !rc)
+ rc = ret;
+ if (rc) {
+ /*
+ * The delay registers were written and the rollback may not
+ * have put all of them back, so what the output realizes is
+ * no longer what the cache says. Mark it so the getter reads
+ * the registers instead of reporting the value that was
+ * cached before this call.
+ */
+ sitdev->out[out_idx].phase_stale = true;
+ return rc;
+ }
+
+ /*
+ * Restart the output divider phase so the freshly programmed delay is
+ * applied against a known edge instead of the divider's arbitrary
+ * running phase.
+ */
+ rc = sit9531x_output_phase_flush(sitdev, pll_idx);
+ if (rc) {
+ /* The delay is programmed but not re-timed; same reasoning. */
+ sitdev->out[out_idx].phase_stale = true;
+ return rc;
+ }
+ sitdev->out[out_idx].phase_stale = false;
+
+cache:
+ /*
+ * Cache what the registers realize, and only once every step has
+ * succeeded: the core drops a repeated request with the same value,
+ * so a cache updated by a failed call would make the retry a no-op.
+ * The encoding above keeps the realized delay below one period.
+ */
+ coarse_ps = mul_u64_u64_div_u64(coarse, 1000000000000ULL, fvco);
+ abs_ps = coarse_ps + (u64)fine * SIT9531X_OUT_PRG_FINE_STEP_PS;
+ /*
+ * Quantization can also land a few picoseconds past the end of the
+ * advertised range, which the getter must not report. Bound both
+ * signs to the range; the positive one is also what keeps the cast
+ * to the s32 the ABI carries safe.
+ */
+ if (phase_norm_ps < 0)
+ sitdev->out[out_idx].phase_adj =
+ abs_ps ? -(s32)min_t(u64, t_out_ps - abs_ps,
+ SIT9531X_OUT_PHASE_ADJ_MAX_PS) : 0;
+ else
+ sitdev->out[out_idx].phase_adj =
+ (s32)min_t(u64, abs_ps, SIT9531X_OUT_PHASE_ADJ_MAX_PS);
+
+ /*
+ * Record whether a delay was asked for, whatever it quantized to: an
+ * advance is held as T_out - |advance|, which the rate change that
+ * follows has to re-encode against the new period. A request of 0
+ * leaves nothing to re-time.
+ */
+ sitdev->out[out_idx].phase_armed = phase_norm_ps != 0;
+
+ return 0;
+}
+
/*
* sit9531x_clear_notifications - clear all notification registers
*
@@ -2753,12 +3151,37 @@ static int sit9531x_dev_state_fetch(struct sit9531x_dev *sitdev)
}
for (i = 0; i < sitdev->info->num_outputs; i++) {
+ s32 phase_ps;
+
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;
}
+
+ /*
+ * The delay registers are part of the profile the chip loads
+ * before probe, so an output can already carry one. Seeding
+ * the cache from the device is what lets a request of 0 ps
+ * clear it: the core drops a request equal to what the
+ * getter reports, and a cache that started at zero would
+ * make clearing a programmed delay impossible. An output
+ * the configuration does not route has no Fvco to decode
+ * against, which is not an error here.
+ */
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_output_phase_read(sitdev, i, &phase_ps);
+ mutex_unlock(&sitdev->multiop_lock);
+ if (!rc) {
+ sitdev->out[i].phase_adj = phase_ps;
+ sitdev->out[i].phase_armed = !!phase_ps;
+ } else if (rc != -ENODATA) {
+ dev_err(sitdev->dev,
+ "Failed to read output %u delay: %d\n",
+ i, rc);
+ return rc;
+ }
}
for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 8e1378d04b77..66c87e8f0387 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -27,6 +27,8 @@
#define SIT9531X_MAX_INPUTS 8
#define SIT9531X_NUM_INPUT_PAIRS (SIT9531X_MAX_INPUTS / 2)
#define SIT9531X_MAX_OUTPUTS 12
+/* Output phase-adjust range advertised to the core, +/-1 ms in ps */
+#define SIT9531X_OUT_PHASE_ADJ_MAX_PS 1000000000
/*
* INTSYNC (the inter-PLL sync net) is modeled as two pins. The
* destination PLL that locks to INTSYNC sees an input pin
@@ -104,6 +106,13 @@ struct sit9531x_ref {
* @routed: output is mapped to @pll_idx by the initial
* configuration; an unrouted output has no DPLL pin
* @pll_idx: PLL driving this output (0-3)
+ * @phase_stale: the programmed delay may differ from @phase_adj
+ * @phase_armed: a phase adjust has been programmed, so a rate
+ * change has to re-time it even when it quantized
+ * to zero
+ * @phase_adj: phase adjust the delay registers actually realize,
+ * i.e. the last request quantized to whole VCO cycles
+ * plus 30 ps fine steps, in the request's sign
*/
struct sit9531x_out {
u64 freq;
@@ -112,6 +121,9 @@ struct sit9531x_out {
bool state_stale;
bool routed;
u8 pll_idx;
+ s32 phase_adj;
+ bool phase_armed;
+ bool phase_stale;
};
/*
@@ -273,6 +285,10 @@ 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_read(struct sit9531x_dev *sitdev, u8 out_idx,
+ s32 *phase_ps);
+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);
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index ac2d977d36f7..8a5e3a1decd0 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -851,12 +851,90 @@ sit9531x_dpll_output_pin_state_on_dpll_set(const struct dpll_pin *pin,
return rc;
}
+/*
+ * sit9531x_dpll_output_pin_phase_adjust_get - read output phase adjustment
+ *
+ * Returns what the delay registers hold, i.e. the value
+ * sit9531x_output_phase_adjust_set() programmed after quantization, read
+ * from the cache unless a failed request left it unconfirmed.
+ */
+static int
+sit9531x_dpll_output_pin_phase_adjust_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, s32 *phase_adjust,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+ /*
+ * A request whose writes reached the device but whose commit or
+ * phase flush failed left the cache describing the delay before it.
+ * There is no poll of the delay registers to correct that, so read
+ * them here rather than report a value the output is not using.
+ */
+ if (sitdev->out[dpin->id].phase_stale) {
+ s32 phase_ps;
+
+ rc = sit9531x_output_phase_read(sitdev, dpin->id, &phase_ps);
+ if (rc) {
+ mutex_unlock(&sitdev->multiop_lock);
+ NL_SET_ERR_MSG(extack,
+ "Output delay could not be read back");
+ return rc;
+ }
+ sitdev->out[dpin->id].phase_adj = phase_ps;
+ sitdev->out[dpin->id].phase_armed = !!phase_ps;
+ sitdev->out[dpin->id].phase_stale = false;
+ }
+ *phase_adjust = sit9531x_out_state_get(sitdev, dpin->id)->phase_adj;
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_output_pin_phase_adjust_set - set output phase adjustment
+ *
+ * Programs the per-output PRG_RST_DELAY registers for deterministic
+ * phase offset; see sit9531x_output_phase_adjust_set() in core.c.
+ */
+static int
+sit9531x_dpll_output_pin_phase_adjust_set(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, s32 phase_adjust,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_output_phase_adjust_set(sitdev, dpin->id, phase_adjust);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc) {
+ NL_SET_ERR_MSG(extack, "Phase adjust failed");
+ return rc;
+ }
+
+ return 0;
+}
+
static const struct dpll_pin_ops sit9531x_dpll_output_pin_ops = {
.direction_get = sit9531x_dpll_output_pin_direction_get,
.frequency_get = sit9531x_dpll_output_pin_frequency_get,
.frequency_set = sit9531x_dpll_output_pin_frequency_set,
.state_on_dpll_get = sit9531x_dpll_output_pin_state_on_dpll_get,
.state_on_dpll_set = sit9531x_dpll_output_pin_state_on_dpll_set,
+ .phase_adjust_get = sit9531x_dpll_output_pin_phase_adjust_get,
+ .phase_adjust_set = sit9531x_dpll_output_pin_phase_adjust_set,
};
const struct dpll_pin_ops *
diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
index 42f3e53b6def..02181e8bbaf8 100644
--- a/drivers/dpll/sit9531x/prop.c
+++ b/drivers/dpll/sit9531x/prop.c
@@ -228,6 +228,26 @@ sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
props->dpll_props.capabilities =
DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
curr_freq = sitdev->out[index].freq;
+
+ /*
+ * Allow phase-adjust over a +/-1 ms window. The subsystem
+ * rejects pin_set(phase-adjust, X) when X falls outside
+ * [min, max], so leaving these at 0 silently blocks every
+ * netlink call. 1 ms is well beyond the DCO dynamic range
+ * but costs nothing. Only outputs get a range: input pins
+ * have no .phase_adjust_set, and advertising one there would
+ * promise userspace something every set would refuse.
+ */
+ props->dpll_props.phase_range.min =
+ -SIT9531X_OUT_PHASE_ADJ_MAX_PS;
+ props->dpll_props.phase_range.max =
+ SIT9531X_OUT_PHASE_ADJ_MAX_PS;
+ /*
+ * The fine step is 30 ps, but requests are accepted at 1 ps
+ * resolution and rounded to the nearest achievable delay, so
+ * advertise the request granularity, not the hardware step.
+ */
+ props->dpll_props.phase_gran = 1;
}
/* Generate package label */
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index d2e1ac547cbc..b805cbc41fc1 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -202,6 +202,42 @@
#define SIT9531X_DEBUG_UNLOCK_VAL 0xC3
#define SIT9531X_DEBUG_LOCK_VAL 0x00
+/*
+ * Per-output programmable phase delay: 34-bit coarse (in VCO clock
+ * cycles) plus a 3-bit fine field with fixed 30 ps steps. Each output
+ * has a five-byte block PROG6..PROG2:
+ *
+ * base + 0 PROG6 [7:5] OPSTG_VCASC_BUMP (preserve via RMW)
+ * [4:2] PRG_RST_FINE_DELAY[2:0]
+ * [1:0] PRG_RST_DELAY[33:32]
+ * base + 1 PROG5 [7:0] PRG_RST_DELAY[31:24]
+ * base + 2 PROG4 [7:0] PRG_RST_DELAY[23:16]
+ * base + 3 PROG3 [7:0] PRG_RST_DELAY[15:8]
+ * base + 4 PROG2 [7:0] PRG_RST_DELAY[7:0]
+ *
+ * Slots 0-5 are on Page 3, slots 6-11 on Page 4. The block base
+ * within a page is 0x15 + 16 * (slot % 6), where slot is the physical
+ * output slot from clkout_map[], not the logical output index.
+ */
+#define SIT9531X_OUT_PRG_DELAY_BASE 0x15
+#define SIT9531X_OUT_PRG_SLOT_STRIDE 0x10
+/* bits [7:5], preserve */
+#define SIT9531X_OUT_PRG_OPSTG_MASK 0xE0
+#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] */
+/*
+ * The divider takes two VCO cycles to act on a programmed delay and
+ * release the output, so the encoded value carries them and the realized
+ * delay is the register value less that. The reference flow adds the
+ * same two.
+ */
+#define SIT9531X_OUT_PRG_DIVO_CYCLES 2
+
+#define SIT9531X_OUT_PRG_FINE_STEP_PS 30
+#define SIT9531X_OUT_PRG_FINE_MAX 7 /* 3-bit field */
+#define SIT9531X_OUT_PRG_COARSE_BITS 34
+
/*
* On-demand phase-flush fired from a register rather than a GPIO pin.
* DIVO_PHASE_SEL_REG selects the in-register trigger source and
--
2.43.0
^ permalink raw reply [flat|nested] 26+ messages in thread* Re: [PATCH net-next v11 11/13] dpll: sit9531x: add support to adjust output phase
2026-09-30 23:37 ` [PATCH net-next v11 11/13] dpll: sit9531x: add support to adjust output phase Ali Rouhi
@ 2026-10-05 1:10 ` netdev-bot+sashiko
0 siblings, 0 replies; 26+ messages in thread
From: netdev-bot+sashiko @ 2026-10-05 1:10 UTC (permalink / raw)
To: arouhi
Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba,
pabeni, robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
devicetree, netdev, linux-kernel
Thank you for your contribution! Sashiko AI review found 10 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 8 · Low: 2
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] Each phase-adjust write calls sit9531x_output_phase_flush()
(core.c:2671).
- [Medium] On a rate change, sit9531x_output_freq_set() re-times from the
cached phase_adj, which may be clamped or folded, rather than from the…
- [Medium] sit9531x_output_phase_read() is not the inverse of the
encoding used by sit9531x_output_phase_adjust_set().
- [Medium] sit9531x_output_phase_read() discards register-access errors
from sit9531x_output_divo_read().
- [Medium] A phase-flush failure that the setter records in phase_stale
is never retried, because the getter clears phase_stale.
- [Medium] sit9531x_output_phase_adjust_set() truncates the output period
to whole picoseconds (`t_out_ps = mul_u64_u64_div_u64(divo, 1e12,…
- [Medium] In sit9531x_output_freq_set(), the new phase re-time and
invalidation block (core.c:2351-2372) is reached only when divo_write…
- [Medium] The new phase code converts VCO cycles to picoseconds with
mul_u64_u64_div_u64(x, 1000000000000ULL, fvco), where x is a 34-bit…
- [Low] The commit message says the ±1 ms window 'is wider than the
dynamic range but costs nothing', and prop.c:236 says '1 ms is well…
- [Low] The quantizer in sit9531x_output_phase_adjust_set() only
considers coarse = floor(abs_ps * Fvco / 1e12) (core.c:2511) and…
--
dpll: sit9531x: add support to adjust output phase
This adds output phase-adjust support to the sit9531x DPLL driver. It
programs the per-output PRG_RST_DELAY coarse and fine registers, and it
folds each request modulo one output period. At probe the phase cache is
seeded from the hardware, advances are re-timed when the rate changes, and
the driver advertises a +/-1 ms range with 1 ps granularity.
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index 8fed14d8dca62..d2719ff24249f 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -2183,6 +2183,110 @@ static int sit9531x_output_divo_read(struct sit9531x_dev *sitdev, u8 out_idx,
[ ... ]
> +int sit9531x_output_phase_read(struct sit9531x_dev *sitdev, u8 out_idx,
> + s32 *phase_ps)
> +{
[ ... ]
> + coarse = (coarse > SIT9531X_OUT_PRG_DIVO_CYCLES) ?
> + coarse - SIT9531X_OUT_PRG_DIVO_CYCLES : 0;
> +
> + ps = mul_u64_u64_div_u64(coarse, 1000000000000ULL, fvco);
[Severity: Medium]
Can this conversion overflow and raise a divide error? Here coarse is a
raw 34-bit value from the device. sit9531x_get_fvco() only rejects
fvco == 0; an out-of-band fvco gets a dev_warn_once() and is returned
anyway.
With a tiny DIVN (for example an integer part of 0 plus a small fraction),
fvco can be a few hundred Hz. If fvco is below about 940 Hz and coarse (or
divo, for t_out_ps below) is near 2^34, then x * 1e12 / fvco does not fit
in u64. On x86-64, mul_u64_u64_div_u64() is a mulq/divq pair, which raises
#DE in that case.
At probe this runs for every routed output through
sit9531x_dev_state_fetch()->sit9531x_output_phase_read(). The same pattern
appears in sit9531x_output_phase_adjust_set() for t_out_ps, coarse_ps,
up_ps and the cached value.
sit9531x_divn_static() already guards against this kind of #DE. Should
these conversions also bound fvco or the result?
> + ps += (u64)fine * SIT9531X_OUT_PRG_FINE_STEP_PS;
[ ... ]
> + if (!sit9531x_output_divo_read(sitdev, out_idx, &divo)) {
[Severity: Medium]
Does this drop register access errors from sit9531x_output_divo_read()?
An I2C or regmap error is handled the same way as -ENODATA, and the
function falls through to:
*phase_ps = (s32)min_t(u64, ps, SIT9531X_OUT_PHASE_ADJ_MAX_PS);
return 0;
The value returned is unfolded, possibly clamped, and never negative. The
kernel-doc promises "<0 on register access error", and the delay byte
reads and sit9531x_get_fvco() above do pass their errors back.
sit9531x_dpll_output_pin_phase_adjust_get() treats rc == 0 as
authoritative:
sitdev->out[dpin->id].phase_adj = phase_ps;
sitdev->out[dpin->id].phase_armed = !!phase_ps;
sitdev->out[dpin->id].phase_stale = false;
So one transient bus error during the stale read-back could leave a 1 PPS
advance of -500000 ps cached as +1e9, with no stale marker. After that,
dpll_pin_phase_adj_set() drops a matching request, and the next rate
change writes the wrong delay. sit9531x_dev_state_fetch() seeds the cache
at probe from the same read.
Should only -ENODATA skip the fold, with other errors returned?
> + u64 t_out_ps = mul_u64_u64_div_u64(divo, 1000000000000ULL,
> + fvco);
> +
> + if (t_out_ps) {
> + div64_u64_rem(ps, t_out_ps, &ps);
> + if (ps > SIT9531X_OUT_PHASE_ADJ_MAX_PS &&
> + t_out_ps - ps <= SIT9531X_OUT_PHASE_ADJ_MAX_PS) {
[Severity: Medium]
Is this the inverse of the encoding in
sit9531x_output_phase_adjust_set()? After the fold, ps < t_out_ps. When
t_out_ps <= SIT9531X_OUT_PHASE_ADJ_MAX_PS, which is every output faster
than 1 kHz, the two conditions can never both hold, so the function never
returns a negative value.
Take a 10 MHz output (T = 100000 ps). A -30000 ps request is written as
70000 ps (plus the two cycles) and cached as -30000. Reading it back gives
+70000.
That read-back happens at probe, and on the phase_stale path in
sit9531x_dpll_output_pin_phase_adjust_get(), which overwrites phase_adj
with +70000 and re-arms it. On the next rate change,
sit9531x_output_freq_set() encodes +70000 as a positive delay instead of
T_new - 30000. The output then sits at a different phase from the one
requested, and the reported phase_adjust changes sign.
The kernel-doc above says such an advance "reads back as that advance".
That does not seem to hold for these outputs.
> + *phase_ps = -(s32)(t_out_ps - ps);
> + return 0;
> + }
> + }
> + }
> + *phase_ps = (s32)min_t(u64, ps, SIT9531X_OUT_PHASE_ADJ_MAX_PS);
> +
> + return 0;
> +}
[ ... ]
> @@ -2236,7 +2340,38 @@ int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
>
> sitdev->out[out_idx].freq = div64_u64(fvco, divo);
>
> - return 0;
> + /*
> + * The programmed reset delay counts VCO cycles, and a rate change
> + * moves only the divider, so a positive delay keeps its timing; an
> + * advance, though, is held as T_out - |advance| and has to be
> + * re-encoded against the new period. Re-encode whatever was asked
> + * for: for a positive delay that lands on the same register bytes,
> + * and sit9531x_output_phase_adjust_set() then writes nothing.
> + */
> + if (sitdev->out[out_idx].phase_armed) {
[Severity: Medium]
Is this block reached on every path where the divider changed? Earlier in
sit9531x_output_freq_set(), the function returns before getting here:
ret = sit9531x_prg_commit(sitdev);
if (ret && !rc)
rc = ret;
if (rc)
return rc;
prg_commit() can report a failed UPDATE_NVM write that still reached the
part. Its loop-lock retries can also fail after the update took effect.
sit9531x_output_divo_write() can fail mid-sequence with a failed rollback,
and the remaining bytes are then committed.
If the period changed on one of those paths, a cached advance held as
T_old - |adv| no longer means -|adv|, but phase_stale stays false. The
getter keeps reporting the old advance, so the core drops a request to
restore that value. A retried frequency_set is dropped too, because the
core reads the new rate first.
Should the error path set phase_stale when phase_armed is set?
> + s32 phase_ps = sitdev->out[out_idx].phase_adj;
> + int ph_rc;
> +
[ ... ]
> + ph_rc = sit9531x_output_phase_adjust_set(sitdev, out_idx,
> + phase_ps);
[Severity: Medium]
How many times does one rate change disturb the PLL here?
sit9531x_output_freq_set() has already called sit9531x_output_phase_flush()
after the DIVO commit.
For any advance, T_new - |adv| differs from T_old - |adv|, so the memcmp
shortcut in sit9531x_output_phase_adjust_set() does not match. The setter
then runs another prg_enter()/prg_commit() cycle, which opens the loops
again and adds another 100 ms settle, followed by another flush. Between
the two flushes the output runs with the old-period encoding.
On a PLL whose CONFIG47 PHFL_EN bit is clear, sit9531x_output_phase_flush()
does this:
if (!(phfl & SIT9531X_PLL_CONFIG47_PHFL_EN))
return sit9531x_write_pll_u8(sitdev, pll_idx,
SIT9531X_PLL_REG_DIRECTIVES,
SIT9531X_PLL_DIRECTIVE_RESTART);
regs.h describes that bit as "the restart bit restarts the PLL". On such a
PLL, every phase-adjust write restarts the whole PLL, and an armed rate
change restarts it twice.
The commit message says:
The device has no per-output phase flush, so realigning the adjusted
output restarts the divider phase of every output that PLL drives.
Should the commit message mention the full PLL restart? And could the rate
change avoid the second programming cycle and flush?
> + if (ph_rc) {
[ ... ]
> @@ -2304,14 +2439,277 @@ int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
[ ... ]
> + t_out_ps = mul_u64_u64_div_u64(divo, 1000000000000ULL, fvco);
> + if (!t_out_ps)
> + return -EINVAL;
> +
[ ... ]
> + abs_ps = abs(phase_ps);
> + div64_u64_rem(abs_ps, t_out_ps, &abs_ps);
[Severity: Medium]
Does truncating the period to whole picoseconds give the wrong fold for
requests longer than one period? The dropped fraction accumulates once for
every period that is folded away.
For example, Fvco = 5.12 GHz with DIVO = 40 gives a 128 MHz output with a
7812.5 ps period, which is truncated to 7812. A 100000000 ps request is
exactly 12800 periods and should fold to 0. Instead,
100000000 % 7812 = 6400, so 6400 ps is programmed on a 7.8 ns period.
Rates such as 19.44 MHz and 122.88 MHz also have periods that are not a
whole number of picoseconds. A 1 ms request at 19.44 MHz is exactly 19440
periods, but it folds to 6400 ps.
The read-back fold in sit9531x_output_phase_read() uses the same truncated
period.
> + phase_norm_ps = phase_ps < 0 ? -(s64)abs_ps : (s64)abs_ps;
> + abs_ps = (phase_ps < 0 && abs_ps) ? t_out_ps - abs_ps : abs_ps;
> +
> + if (abs_ps) {
> + u64 rem_ps, err, up_ps;
> +
[ ... ]
> + coarse = mul_u64_u64_div_u64(abs_ps, fvco, 1000000000000ULL);
> +
[ ... ]
> + 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;
> + }
> + err = abs_diff(abs_ps, coarse_ps +
> + (u64)fine * SIT9531X_OUT_PRG_FINE_STEP_PS);
> + up_ps = mul_u64_u64_div_u64(coarse + 1, 1000000000000ULL,
> + fvco);
> + if (up_ps - abs_ps < err) {
> + coarse++;
> + fine = 0;
> + }
[Severity: Low]
Is the previous coarse cycle ever considered here? The fine field goes up
to 7 * 30 = 210 ps, which is longer than one VCO period in every
supported band. So coarse - 1 with a large fine code can be closer, or
even exact.
At 5 GHz (200 ps per cycle), a 210 ps request gives coarse = 1 with a
10 ps remainder. fine rounds to 0, and 200 ps is programmed, even though
coarse = 0 with fine = 7 gives exactly 210 ps. prop.c says requests are
"rounded to the nearest achievable delay".
The quantizer is also not idempotent for such encodings. A profile holding
coarse 0 / fine 7 seeds phase_adj = 210 and phase_armed = true. The next
frequency_set re-encodes it as 200 ps, which rewrites the registers and
flushes the PLL.
[ ... ]
> + rc = sit9531x_output_phase_flush(sitdev, pll_idx);
> + if (rc) {
> + /* The delay is programmed but not re-timed; same reasoning. */
> + sitdev->out[out_idx].phase_stale = true;
> + return rc;
> + }
> + sitdev->out[out_idx].phase_stale = false;
[Severity: Medium]
Is a failed flush here ever retried? The delay bytes can be committed and
then sit9531x_output_phase_flush() can fail, for example on its first
CONFIG47 read. When that happens, phase_stale is the only record of the
failure.
On a retry, dpll_pin_phase_adj_set() calls phase_adjust_get() first.
Because phase_stale is set, sit9531x_dpll_output_pin_phase_adjust_get()
reads the newly programmed bytes, publishes that value and clears
phase_stale. It does not redo the flush.
For an on-grid request, the core then sees:
if (phase_adj == old_phase_adj)
return 0;
and never calls the setter. For an off-grid request the setter does run,
but phase_stale is already clear, so it takes the shortcut:
if (!memcmp(old_bytes, new_bytes, sizeof(new_bytes)) &&
!sitdev->out[out_idx].phase_stale)
goto cache;
Either way the realignment never happens, although phase_stale exists to
force a rewrite after this kind of failure. Should a pending flush be
tracked separately from an unconfirmed cache?
[ ... ]
> @@ -2753,12 +3151,37 @@ static int sit9531x_dev_state_fetch(struct sit9531x_dev *sitdev)
[ ... ]
> + mutex_lock(&sitdev->multiop_lock);
> + rc = sit9531x_output_phase_read(sitdev, i, &phase_ps);
> + mutex_unlock(&sitdev->multiop_lock);
> + if (!rc) {
> + sitdev->out[i].phase_adj = phase_ps;
> + sitdev->out[i].phase_armed = !!phase_ps;
[Severity: Medium]
Can this seed a clamped value that a later rate change then writes to the
device? sit9531x_output_phase_read() saturates to +1 ms and returns
success when both the delay D and T_out - D are larger than
SIT9531X_OUT_PHASE_ADJ_MAX_PS.
Take a 1 PPS output whose profile holds a 10 ms reset delay. Probe stores
phase_adj = 1000000000 and phase_armed = true. The next unrelated
frequency_set on that output calls
sit9531x_output_phase_adjust_set(1000000000) from
sit9531x_output_freq_set(). That encodes different bytes, so the driver
writes, commits and flushes a 1 ms delay, and the profile's 10 ms delay is
lost. Before this patch, the frequency setter did not touch these
registers.
The getter also reports +1 ms while the hardware holds 10 ms. A request
for exactly +1 ms is therefore dropped by dpll_pin_phase_adj_set() and
never reaches the device.
Two comments seem to overstate the design:
- sit9531x_output_freq_set() says it re-encodes "whatever was asked for",
but it actually re-encodes the folded or clamped cache.
- The @phase_armed doc in core.h says a rate change re-times the delay
"even when it quantized to zero". In that case the cached value is 0, so
the re-time programs 0 and clears phase_armed.
[ ... ]
> diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
> index 42f3e53b6def2..02181e8bbaf8f 100644
> --- a/drivers/dpll/sit9531x/prop.c
> +++ b/drivers/dpll/sit9531x/prop.c
> @@ -228,6 +228,26 @@ sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
> props->dpll_props.capabilities =
> DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
> curr_freq = sitdev->out[index].freq;
> +
> + /*
> + * Allow phase-adjust over a +/-1 ms window. The subsystem
> + * rejects pin_set(phase-adjust, X) when X falls outside
> + * [min, max], so leaving these at 0 silently blocks every
> + * netlink call. 1 ms is well beyond the DCO dynamic range
> + * but costs nothing. Only outputs get a range: input pins
[Severity: Low]
Is "well beyond the DCO dynamic range" accurate? The PRG_RST_DELAY coarse
field is 34 bits of VCO cycles, about 3.4 s at 5 GHz, and requests are
folded modulo one output period. On a 1 PPS output, the hardware can
therefore realize delays anywhere in the 1 s period. The setter's own
comment mentions "abs_ps approaches one second of 1 PPS wrap-around".
On slow outputs, the +/-1 ms window is narrower than what the hardware can
do. The real limit is the s32 picosecond uAPI (about +/-2.147 ms), not the
device.
The commit message makes the same claim:
The window advertised to the core is one millisecond either way, which is
wider than the dynamic range but costs nothing
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260930233714.87679-1-arouhi%40sitime.com
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH net-next v11 13/13] dpll: sit9531x: model the inter-PLL sync net as a pair of pins
2026-09-30 23:37 [PATCH net-next v11 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (11 preceding siblings ...)
2026-09-30 23:37 ` [PATCH net-next v11 11/13] dpll: sit9531x: add support to adjust output phase Ali Rouhi
@ 2026-09-30 23:37 ` Ali Rouhi
2026-10-05 1:10 ` netdev-bot+sashiko
12 siblings, 1 reply; 26+ messages in thread
From: Ali Rouhi @ 2026-09-30 23:37 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi, devicetree,
netdev, linux-kernel
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
The device has an internal net by which one PLL can drive the others: the
source PLL puts its output on it, and any other PLL can select it as a
reference instead of an external input. The two ends are nothing alike --
one is driven, the other is selected -- so they are two pins rather than
one: an output pin on the source and an input pin on each destination.
That keeps each pin honest about what its state means. The source pin
reports whether this PLL is the one driving the net, and setting it takes
the net over or gives it up; a destination pin reports whether its PLL has
selected the net, and behaves like any other selectable input. A single
pin would have had to answer both questions at once and could only have
been right about one of them.
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
drivers/dpll/sit9531x/core.c | 345 ++++++++++++++++++++++++++++++++++-
drivers/dpll/sit9531x/core.h | 3 +
drivers/dpll/sit9531x/dpll.c | 272 ++++++++++++++++++++++++++-
drivers/dpll/sit9531x/regs.h | 3 +
4 files changed, 620 insertions(+), 3 deletions(-)
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 45baa703aa45..6b607f3d5fc3 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -2760,6 +2760,338 @@ int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
return 0;
}
+/*
+ * Close the debug window on a PLL's EXT page. The key register opens
+ * every debug register on that page while it holds the unlock value.
+ */
+static int sit9531x_intsync_debug_lock(struct sit9531x_dev *sitdev, u8 ext_page)
+{
+ return sit9531x_write_u8(sitdev,
+ SIT9531X_REG(ext_page, SIT9531X_PLL_REG_DEBUG),
+ SIT9531X_PLL_DEBUG_LOCK);
+}
+
+/*
+ * INTSYNC configuration register values.
+ * These are written to the source PLL's EXT page to enable/disable
+ * inter-PLL synchronization (lock frequency PLL to phase PLL).
+ */
+struct sit9531x_intsync_reg {
+ u8 offset;
+ u8 en_val;
+ u8 dis_val;
+};
+
+static const struct sit9531x_intsync_reg intsync_config[] = {
+ { 0x2D, 0x02, 0x00 },
+ { 0x50, 0x08, 0x00 },
+ { 0x51, 0x04, 0x00 },
+ { 0x54, 0x02, 0x00 },
+ { 0x55, 0x28, 0x20 },
+ { 0x5C, 0x0F, 0x00 },
+ { 0x5D, 0xFF, 0x00 },
+ { 0x6C, 0xDD, 0x00 },
+};
+
+int sit9531x_intsync_src_detect(struct sit9531x_dev *sitdev)
+{
+ s8 src = -1, partial = -1;
+ u8 global;
+ u8 pll, ext_page;
+ int rc, ret, n_en, n_dis;
+ unsigned int i;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &global);
+ if (rc)
+ return rc;
+
+ if (!(global & BIT(SIT9531X_INTSYNC_EN_BIT))) {
+ sitdev->intsync_src = -1;
+ return 0;
+ }
+
+ for (pll = 0; pll < SIT9531X_NUM_PLLS; pll++) {
+ ext_page = SIT9531X_PLL_EXT_PAGE(pll);
+
+ /*
+ * These are debug registers, which the enable and disable
+ * sequences only touch with the EXT page's debug window
+ * open; read them the same way.
+ */
+ rc = sit9531x_write_u8(sitdev,
+ SIT9531X_REG(ext_page,
+ SIT9531X_PLL_REG_DEBUG),
+ SIT9531X_PLL_DEBUG_UNLOCK);
+ if (rc)
+ return rc;
+
+ n_en = 0;
+ n_dis = 0;
+ for (i = 0; i < ARRAY_SIZE(intsync_config); i++) {
+ u16 reg;
+ u8 val;
+
+ reg = SIT9531X_REG(ext_page, intsync_config[i].offset);
+
+ rc = sit9531x_read_u8(sitdev, reg, &val);
+ if (rc)
+ break;
+ if (val == intsync_config[i].en_val)
+ n_en++;
+ else if (val == intsync_config[i].dis_val)
+ n_dis++;
+ }
+
+ ret = sit9531x_intsync_debug_lock(sitdev, ext_page);
+ if (!rc)
+ rc = ret;
+ if (rc)
+ return rc;
+
+ if (n_en == ARRAY_SIZE(intsync_config)) {
+ /*
+ * Only one PLL can drive the net. If a second
+ * one matches, the registers are not describing
+ * a state this driver put the device in, so say
+ * so rather than pick silently.
+ */
+ if (src < 0)
+ src = pll;
+ else
+ dev_warn(sitdev->dev,
+ "PLL%c also matches the INTSYNC source pattern; keeping PLL%c\n",
+ 'A' + pll, 'A' + src);
+ } else if (n_en && n_dis &&
+ n_en + n_dis == ARRAY_SIZE(intsync_config) &&
+ partial < 0) {
+ /*
+ * Every register holds one of the two patterns and both
+ * occur: the residue of an enable or a disable that
+ * stopped part way, and nothing a profile writes.
+ */
+ partial = pll;
+ }
+ }
+
+ /*
+ * A disable that stopped part way leaves a PLL holding a mix of the
+ * two patterns. Treat it as the owner, so that a repeated disconnect
+ * on that PLL runs the disable again and another PLL cannot be
+ * enabled on top of the leftover configuration.
+ */
+ if (src < 0 && partial >= 0) {
+ dev_warn(sitdev->dev,
+ "PLL%c holds a partial INTSYNC source configuration\n",
+ 'A' + partial);
+ src = partial;
+ }
+
+ sitdev->intsync_src = src;
+
+ return 0;
+}
+
+/*
+ * sit9531x_intsync_enable - enable inter-PLL synchronization
+ * @src_pll_idx: source (frequency) PLL index (0-3)
+ *
+ * Enables INTSYNC global bit, unlocks the source PLL's EXT page
+ * debug registers, writes configuration, and triggers a small
+ * update on the source PLL.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_intsync_enable(struct sit9531x_dev *sitdev, u8 src_pll_idx)
+{
+ u8 ext_page, val;
+ int rc, lock_rc;
+ unsigned int i;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (src_pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+
+ ext_page = SIT9531X_PLL_EXT_PAGE(src_pll_idx);
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &val);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL,
+ val | BIT(SIT9531X_INTSYNC_EN_BIT));
+ if (rc)
+ return rc;
+
+ /* Small update on Page 0 */
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+ SIT9531X_SMALL_UPDATE_CMD);
+ usleep_range(1000, 2000);
+ if (rc)
+ goto relock_err;
+
+ /* Unlock debug on EXT page */
+ rc = sit9531x_write_u8(sitdev,
+ SIT9531X_REG(ext_page,
+ SIT9531X_PLL_REG_DEBUG),
+ SIT9531X_PLL_DEBUG_UNLOCK);
+ if (rc)
+ goto relock_err;
+
+ for (i = 0; i < ARRAY_SIZE(intsync_config); i++) {
+ rc = sit9531x_write_u8(sitdev,
+ SIT9531X_REG(ext_page,
+ intsync_config[i].offset),
+ intsync_config[i].en_val);
+ if (rc)
+ goto relock_err;
+ }
+
+ /* Small update on source PLL */
+ rc = sit9531x_pll_small_update(sitdev, src_pll_idx);
+ if (rc)
+ goto relock_err;
+
+ rc = 0;
+ goto relock;
+
+relock_err:
+ sit9531x_intsync_debug_lock(sitdev, ext_page);
+ goto err_disable;
+
+relock:
+ /*
+ * Close the EXT page debug window the sequence opened. Nothing
+ * else writes the key back, so leaving it open would keep the block
+ * unlocked for as long as the device runs. The net is driven by
+ * now, though, so a failure here must not read as a failed enable:
+ * the caller would then not record the owner of a net that is
+ * driven.
+ */
+ lock_rc = sit9531x_intsync_debug_lock(sitdev, ext_page);
+ if (lock_rc)
+ dev_warn(sitdev->dev,
+ "PLL%c: INTSYNC enabled but the debug window was left open: %d\n",
+ 'A' + src_pll_idx, lock_rc);
+
+ return rc;
+
+err_disable:
+ /*
+ * The global enable is already set at this point. The caller only
+ * records the source PLL when this function succeeds, so nothing
+ * else will ever clear the bit: undo it here rather than leave the
+ * net asserted with a half-written EXT page.
+ */
+ {
+ int rollback_rc;
+
+ rollback_rc = sit9531x_intsync_disable(sitdev, src_pll_idx);
+ if (rollback_rc)
+ dev_warn(sitdev->dev,
+ "INTSYNC rollback failed after enable error: %d (original %d)\n",
+ rollback_rc, rc);
+ }
+
+ return rc;
+}
+
+/*
+ * sit9531x_intsync_disable - disable inter-PLL synchronization
+ * @src_pll_idx: source (frequency) PLL index (0-3)
+ *
+ * Clears INTSYNC global bit, writes disable values to the source
+ * PLL's EXT page, and triggers a small update.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_intsync_disable(struct sit9531x_dev *sitdev, u8 src_pll_idx)
+{
+ u8 ext_page, val;
+ int rc, lock_rc;
+ unsigned int i;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (src_pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+
+ ext_page = SIT9531X_PLL_EXT_PAGE(src_pll_idx);
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &val);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL,
+ val & ~BIT(SIT9531X_INTSYNC_EN_BIT));
+ if (rc)
+ goto restore_global;
+
+ /* Small update on Page 0 */
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+ SIT9531X_SMALL_UPDATE_CMD);
+ usleep_range(1000, 2000);
+ if (rc)
+ goto restore_global;
+
+ /* Unlock debug on EXT page */
+ rc = sit9531x_write_u8(sitdev,
+ SIT9531X_REG(ext_page,
+ SIT9531X_PLL_REG_DEBUG),
+ SIT9531X_PLL_DEBUG_UNLOCK);
+ if (rc)
+ goto restore_global;
+
+ 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)
+ goto restore_global;
+ }
+
+ /* Small update on source PLL */
+ rc = sit9531x_pll_small_update(sitdev, src_pll_idx);
+ if (rc)
+ goto restore_global;
+
+ goto relock;
+
+restore_global:
+ /*
+ * The global enable was cleared first, so a failure here leaves the
+ * EXT page still holding the enable pattern with nothing pointing
+ * at it: the source detector keys on the global bit, would report
+ * the net as unowned, and a retry of the disable would then
+ * short-circuit. Put the bit back so the state stays one the
+ * driver can describe and the request can be repeated.
+ */
+ if (!sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &val))
+ sit9531x_write_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL,
+ val | BIT(SIT9531X_INTSYNC_EN_BIT));
+
+relock:
+ /*
+ * Close the EXT page debug window the sequence opened. After a
+ * disable that took effect, a failure here is a warning, for the
+ * same reason as in sit9531x_intsync_enable().
+ */
+ lock_rc = sit9531x_intsync_debug_lock(sitdev, ext_page);
+ if (lock_rc) {
+ if (rc)
+ dev_warn(sitdev->dev,
+ "PLL%c: debug window left open: %d\n",
+ 'A' + src_pll_idx, lock_rc);
+ else
+ dev_warn(sitdev->dev,
+ "PLL%c: INTSYNC disabled but the debug window was left open: %d\n",
+ 'A' + src_pll_idx, lock_rc);
+ }
+
+ return rc;
+}
+
/**
* sit9531x_chan_selected_ref_read - read a PLL's active reference now
* @sitdev: device pointer
@@ -3379,6 +3711,15 @@ static int sit9531x_dev_state_fetch(struct sit9531x_dev *sitdev)
return rc;
}
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_intsync_src_detect(sitdev);
+ mutex_unlock(&sitdev->multiop_lock);
+ if (rc) {
+ dev_err(sitdev->dev,
+ "Failed to detect INTSYNC source: %d\n", rc);
+ return rc;
+ }
+
for (i = 0; i < sitdev->info->num_outputs; i++) {
s32 phase_ps;
@@ -4081,13 +4422,13 @@ static bool sit9531x_dpll_pin_is_registrable(struct sit9531x_dpll *sitdpll,
if (index == SIT9531X_MAX_INPUTS)
return true;
if (index == SIT9531X_INTSYNC_PIN_ID)
- return false;
+ return true;
return sit9531x_input_pin_is_registrable(sitdev, index);
}
if (index == SIT9531X_INTSYNC_OUT_PIN_ID)
- return false;
+ return true;
if (index >= sitdev->info->num_outputs)
return false;
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index bebdc9f5abce..0b6a787b4afb 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -294,6 +294,9 @@ int sit9531x_output_phase_adjust_set(struct sit9531x_dev *sitdev,
int sit9531x_clear_notifications(struct sit9531x_dev *sitdev);
/* ---- INTSYNC (inter-PLL synchronization) ---- */
+int sit9531x_intsync_enable(struct sit9531x_dev *sitdev, u8 src_pll_idx);
+int sit9531x_intsync_disable(struct sit9531x_dev *sitdev, u8 src_pll_idx);
+int sit9531x_intsync_src_detect(struct sit9531x_dev *sitdev);
/* ---- Phase offset (TDC readback) ---- */
int sit9531x_chan_selected_ref_read(struct sit9531x_dev *sitdev, u8 pll_idx,
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index 6bf4efd3c633..52d5b1a6e36f 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -27,6 +27,20 @@ static bool sit9531x_dpll_is_input_pin(const struct sit9531x_dpll_pin *pin)
return pin->dir == DPLL_PIN_DIRECTION_INPUT;
}
+static bool
+sit9531x_dpll_is_intsync_pin(const struct sit9531x_dpll_pin *pin)
+{
+ return sit9531x_dpll_is_input_pin(pin) &&
+ pin->id == SIT9531X_INTSYNC_PIN_ID;
+}
+
+static bool
+sit9531x_dpll_is_intsync_src_pin(const struct sit9531x_dpll_pin *pin)
+{
+ return !sit9531x_dpll_is_input_pin(pin) &&
+ pin->id == SIT9531X_INTSYNC_OUT_PIN_ID;
+}
+
static bool
sit9531x_dpll_is_xo_pin(const struct sit9531x_dpll_pin *pin)
{
@@ -772,8 +786,259 @@ sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
enum dpll_pin_direction *direction,
struct netlink_ext_ack *extack);
+static int
+sit9531x_dpll_intsync_src_state_on_dpll_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state *state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+
+ mutex_lock(&sitdev->multiop_lock);
+ if (sitdev->intsync_src == sitdpll->id)
+ *state = DPLL_PIN_STATE_CONNECTED;
+ else
+ *state = DPLL_PIN_STATE_DISCONNECTED;
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_intsync_src_state_on_dpll_set - drive INTSYNC from a PLL
+ *
+ * CONNECTED -> this PLL drives the INTSYNC net
+ * DISCONNECTED -> stop driving INTSYNC if this PLL drives it
+ *
+ * SELECTABLE is rejected: driving the net is an explicit output routing,
+ * not an automatic-selection candidate, matching the regular output pin.
+ */
+static int
+sit9531x_dpll_intsync_src_state_on_dpll_set(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ int rc = 0, detect_rc = 0;
+ bool changed = false;
+ u8 hw_src;
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ switch (state) {
+ case DPLL_PIN_STATE_CONNECTED:
+ if (sitdev->intsync_src == sitdpll->id)
+ break;
+ if (sitdev->intsync_src >= 0) {
+ NL_SET_ERR_MSG(extack,
+ "INTSYNC is already sourced by another PLL");
+ rc = -EBUSY;
+ break;
+ }
+ /*
+ * A PLL that already lists INTSYNC among its references must
+ * not also drive it: the destination side refuses the mirror
+ * of this, and without the check here the net could be routed
+ * back into the PLL feeding it.
+ */
+ hw_src = sit9531x_input_hw_src(SIT9531X_INTSYNC_PIN_ID);
+ if (sit9531x_input_prio_present(sitdev, sitdpll->id, hw_src)) {
+ NL_SET_ERR_MSG(extack,
+ "PLL selects INTSYNC as a reference; it cannot drive it");
+ rc = -EBUSY;
+ break;
+ }
+ rc = sit9531x_intsync_enable(sitdev, sitdpll->id);
+ changed = true;
+ break;
+ case DPLL_PIN_STATE_DISCONNECTED:
+ if (sitdev->intsync_src != sitdpll->id)
+ break;
+ rc = sit9531x_intsync_disable(sitdev, sitdpll->id);
+ changed = true;
+ break;
+ default:
+ rc = -EINVAL;
+ break;
+ }
+
+ /*
+ * Record what was done before confirming it, and only when this PLL
+ * actually changed: the core forwards a request for the state the
+ * pin is already in, and a DISCONNECTED on a PLL that never drove the
+ * net must not erase the owner. The refresh below leaves the cache
+ * untouched when a read fails, and a cache that wrongly says nobody
+ * drives the net would let a second PLL be configured to drive it.
+ */
+ if (changed && !rc)
+ sitdev->intsync_src = state == DPLL_PIN_STATE_CONNECTED ?
+ sitdpll->id : -1;
+
+ /*
+ * Re-scan hardware after a transition so the cache follows a
+ * partially failed enable or disable as closely as possible.
+ */
+ if (changed)
+ detect_rc = sit9531x_intsync_src_detect(sitdev);
+ /*
+ * The refresh only re-reads what the device now shows. Failing
+ * the request because that read hit a bus error would tell
+ * userspace the enable did not happen when it did.
+ */
+ if (detect_rc)
+ dev_warn(sitdev->dev,
+ "INTSYNC source cache not refreshed: %d\n",
+ detect_rc);
+
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc && rc != -EBUSY && rc != -EINVAL && rc != -EOPNOTSUPP)
+ NL_SET_ERR_MSG(extack, "Failed to set INTSYNC source state");
+
+ return rc;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_intsync_src_pin_ops = {
+ .direction_get = sit9531x_dpll_output_pin_direction_get,
+ .state_on_dpll_get = sit9531x_dpll_intsync_src_state_on_dpll_get,
+ .state_on_dpll_set = sit9531x_dpll_intsync_src_state_on_dpll_set,
+};
+
/* ---- INTSYNC destination (input) pin ---- */
+/*
+ * sit9531x_dpll_intsync_dst_state_on_dpll_get - INTSYNC reference state
+ *
+ * Selection role, so the contract above decides this exactly as it does
+ * for a physical input: the priority table is the eligibility record, and
+ * whether a source PLL happens to be driving the net right now is no more
+ * a state than a momentary LOS is on an external reference. The one
+ * addition is that the PLL driving INTSYNC is never its own destination.
+ */
+static int
+sit9531x_dpll_intsync_dst_state_on_dpll_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state *state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+
+ mutex_lock(&sitdev->multiop_lock);
+ if (sitdev->intsync_src == sitdpll->id)
+ *state = DPLL_PIN_STATE_DISCONNECTED;
+ else
+ sit9531x_dpll_selection_state_get(sitdev, sitdpll,
+ SIT9531X_INTSYNC_PIN_ID,
+ state);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_intsync_dst_state_on_dpll_set - lock a PLL to INTSYNC
+ *
+ * Selection role, so this accepts and refuses what a physical input does,
+ * CONNECTED included: the device pins no reference on request whichever
+ * source is asked for. INTSYNC is an internal net with no physical
+ * receiver, so only the per-PLL priority table is touched; the source pin
+ * controls generation.
+ */
+static int
+sit9531x_dpll_intsync_dst_state_on_dpll_set(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ u8 hw_src = sit9531x_input_hw_src(SIT9531X_INTSYNC_PIN_ID);
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ switch (state) {
+ case DPLL_PIN_STATE_DISCONNECTED:
+ rc = sit9531x_input_prio_remove(sitdev, sitdpll->id, hw_src);
+ break;
+ case DPLL_PIN_STATE_CONNECTED:
+ NL_SET_ERR_MSG(extack,
+ "Device selects its reference by priority; use selectable");
+ rc = -EOPNOTSUPP;
+ break;
+ case DPLL_PIN_STATE_SELECTABLE:
+ if (sitdev->intsync_src == sitdpll->id) {
+ NL_SET_ERR_MSG(extack,
+ "PLL cannot lock to the INTSYNC it drives");
+ rc = -EINVAL;
+ break;
+ }
+ rc = sit9531x_input_prio_add(sitdev, sitdpll->id, hw_src);
+ break;
+ default:
+ rc = -EINVAL;
+ break;
+ }
+
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc == -ENOSPC)
+ NL_SET_ERR_MSG(extack,
+ "Priority table is full of unique sources on this PLL");
+ else if (rc && rc != -EINVAL && rc != -EOPNOTSUPP)
+ NL_SET_ERR_MSG(extack, "Failed to set INTSYNC input state");
+
+ return rc;
+}
+
+static int
+sit9531x_dpll_intsync_dst_operstate_on_dpll_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_operstate *state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+
+ mutex_lock(&sitdev->multiop_lock);
+ sit9531x_dpll_selection_operstate_get(sitdev, sitdpll,
+ SIT9531X_INTSYNC_PIN_ID,
+ state);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return 0;
+}
+
+/*
+ * 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,
+ .operstate_on_dpll_get =
+ sit9531x_dpll_intsync_dst_operstate_on_dpll_get,
+ .prio_get = sit9531x_dpll_input_pin_prio_get,
+ .prio_set = sit9531x_dpll_input_pin_prio_set,
+};
+
/*
* XO (crystal oscillator) pin ops
*
@@ -1070,8 +1335,13 @@ static const struct dpll_pin_ops sit9531x_dpll_output_pin_ops = {
const struct dpll_pin_ops *
sit9531x_dpll_pin_ops_get(const struct sit9531x_dpll_pin *pin)
{
- if (!sit9531x_dpll_is_input_pin(pin))
+ if (!sit9531x_dpll_is_input_pin(pin)) {
+ if (sit9531x_dpll_is_intsync_src_pin(pin))
+ return &sit9531x_dpll_intsync_src_pin_ops;
return &sit9531x_dpll_output_pin_ops;
+ }
+ if (sit9531x_dpll_is_intsync_pin(pin))
+ return &sit9531x_dpll_intsync_dst_pin_ops;
if (sit9531x_dpll_is_xo_pin(pin))
return &sit9531x_dpll_xo_pin_ops;
return &sit9531x_dpll_input_pin_ops;
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index 0ba3c73aa3ed..c0e7c22a3e56 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -324,6 +324,9 @@
#define SIT9531X_PLL_REG_ACTIVE 0x02
#define SIT9531X_PLL_ACTIVE_BIT BIT(0) /* PLL reached active state */
+/* PLL EXT page INTSYNC configuration registers */
+#define SIT9531X_PLL_EXT_PAGE(_idx) (SIT9531X_PAGE_PLLA_EXT + (_idx))
+
#define SIT9531X_PLL_STATUS_OUTER_DIS BIT(5)
/*
--
2.43.0
^ permalink raw reply [flat|nested] 26+ messages in thread* Re: [PATCH net-next v11 13/13] dpll: sit9531x: model the inter-PLL sync net as a pair of pins
2026-09-30 23:37 ` [PATCH net-next v11 13/13] dpll: sit9531x: model the inter-PLL sync net as a pair of pins Ali Rouhi
@ 2026-10-05 1:10 ` netdev-bot+sashiko
0 siblings, 0 replies; 26+ messages in thread
From: netdev-bot+sashiko @ 2026-10-05 1:10 UTC (permalink / raw)
To: arouhi
Cc: jiri, vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba,
pabeni, robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
devicetree, netdev, linux-kernel
Thank you for your contribution! Sashiko AI review found 7 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 5 · Low: 2
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] The INTSYNC destination pin reports operstate STANDBY even
when no PLL drives the net.
- [Medium] sit9531x_dpll_intsync_src_state_on_dpll_set() never sends a
pin-change notification when a request fails but the cached ownership,…
- [Medium] sit9531x_intsync_disable() jumps to restore_global on any
failure and sets SIT9531X_INTSYNC_EN_BIT again, whichever step failed.
- [Medium] A failed enable followed by a failed rollback and a failed re-
detect leaves the net marked unowned while it is configured.
- [Medium] restore_global in sit9531x_intsync_disable() writes
SIT9531X_INTSYNC_EN_BIT back into page-0 reg 0x40 but does not issue…
- [Low] A partially configured source looks exactly like a fully
connected one.
- [Low] INTSYNC ownership is not re-detected on resume.
--
dpll: sit9531x: model the inter-PLL sync net as a pair of pins
This commit models the internal INTSYNC net as two DPLL pins. The source
PLL gets an output pin that drives the net, and each destination PLL gets
an input pin that selects the net by priority. The commit also adds the
register sequences that enable and disable inter-PLL sync, and it detects
the current source at probe.
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index 45baa703aa45b..6b607f3d5fc31 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -2760,6 +2760,338 @@ int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
[ ... ]
> +int sit9531x_intsync_src_detect(struct sit9531x_dev *sitdev)
> +{
[ ... ]
> + } else if (n_en && n_dis &&
> + n_en + n_dis == ARRAY_SIZE(intsync_config) &&
> + partial < 0) {
> + /*
> + * Every register holds one of the two patterns and both
> + * occur: the residue of an enable or a disable that
> + * stopped part way, and nothing a profile writes.
> + */
> + partial = pll;
> + }
> + }
> +
> + /*
> + * A disable that stopped part way leaves a PLL holding a mix of the
> + * two patterns. Treat it as the owner, so that a repeated disconnect
> + * on that PLL runs the disable again and another PLL cannot be
> + * enabled on top of the leftover configuration.
> + */
> + if (src < 0 && partial >= 0) {
> + dev_warn(sitdev->dev,
> + "PLL%c holds a partial INTSYNC source configuration\n",
> + 'A' + partial);
> + src = partial;
> + }
[Severity: Low]
Once this runs, is there any way to tell a half-configured source from a
fully connected one?
Suppose a DISCONNECTED on the owning PLL writes the first dis_val and then
fails on a later write. restore_global sets the global bit again. This
code then sees the mixed pattern and records the PLL as intsync_src.
After that, sit9531x_dpll_intsync_src_state_on_dpll_get() reports
CONNECTED. But the page-0 small update earlier in the disable has already
latched the global enable off.
A later CONNECTED on the same PLL hits this check in
sit9531x_dpll_intsync_src_state_on_dpll_set():
if (sitdev->intsync_src == sitdpll->id)
break;
It returns success without running the enable sequence again. Does the
configuration ever get completed in that case?
Another DISCONNECTED does run the disable again, so this state can be
recovered from.
[ ... ]
> +err_disable:
> + /*
> + * The global enable is already set at this point. The caller only
> + * records the source PLL when this function succeeds, so nothing
> + * else will ever clear the bit: undo it here rather than leave the
> + * net asserted with a half-written EXT page.
> + */
> + {
> + int rollback_rc;
> +
> + rollback_rc = sit9531x_intsync_disable(sitdev, src_pll_idx);
> + if (rollback_rc)
> + dev_warn(sitdev->dev,
> + "INTSYNC rollback failed after enable error: %d (original %d)\n",
> + rollback_rc, rc);
> + }
[Severity: Medium]
What happens to the cached owner if this rollback also fails?
Start with intsync_src == -1. The enable latches the global bit, writes
all eight en_val registers, and then fails in sit9531x_pll_small_update().
The rollback disable returns at once if its first read fails:
rc = sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &val);
if (rc)
return rc;
That leaves the global bit and the EXT enable pattern in place. The caller
does not update the cache because rc != 0. If
sit9531x_intsync_src_detect() then also fails its first read, intsync_src
stays at -1. Only probe and the setter call detect, so nothing corrects it
later.
When the bus recovers, a DISCONNECTED on this PLL returns 0 without any
cleanup, because intsync_src != id. A CONNECTED on another PLL passes the
intsync_src >= 0 check and programs a second source.
Isn't this the case the setter's comment says it has to prevent ("a cache
that wrongly says nobody drives the net would let a second PLL be
configured to drive it")?
[ ... ]
> +int sit9531x_intsync_disable(struct sit9531x_dev *sitdev, u8 src_pll_idx)
> +{
[ ... ]
> + /* Small update on source PLL */
> + rc = sit9531x_pll_small_update(sitdev, src_pll_idx);
> + if (rc)
> + goto restore_global;
[Severity: Medium]
If only this last step fails, should restore_global still set the global
bit again?
By this point the page-0 small update has latched the cleared bit, and
all eight registers hold dis_val. restore_global below sets
SIT9531X_INTSYNC_EN_BIT again anyway.
sit9531x_dpll_intsync_src_state_on_dpll_set() leaves the cache alone
because rc != 0, but it then calls sit9531x_intsync_src_detect(). Since
the global bit is set, detect scans the EXT pages. This PLL shows
n_dis == 8 and n_en == 0. That matches neither the full pattern nor the
partial one, so intsync_src becomes -1.
A retried DISCONNECTED then hits this check:
if (sitdev->intsync_src != sitdpll->id)
break;
It returns 0 without touching hardware, even though the PLL small update
that applies the disable values never ran. A CONNECTED on another PLL
also gets past the -EBUSY check and enables INTSYNC on top of this state.
Doesn't that go against the restore_global comment, which says the state
stays "one the driver can describe and the request can be repeated"?
The same restore also seems to undo the rollback in
sit9531x_intsync_enable(). Suppose the enable fails at its page-0 small
update or at the EXT unlock, so err_disable calls
sit9531x_intsync_disable(). If that disable then fails at its global
write or its page-0 update, restore_global sets the bit err_disable was
meant to clear.
The EXT page then holds no enable pattern, so detect reports no owner.
The global enable stays asserted, and the driver has no owner on record
that would ever clear it.
> +
> + goto relock;
> +
> +restore_global:
> + /*
> + * The global enable was cleared first, so a failure here leaves the
> + * EXT page still holding the enable pattern with nothing pointing
> + * at it: the source detector keys on the global bit, would report
> + * the net as unowned, and a retry of the disable would then
> + * short-circuit. Put the bit back so the state stays one the
> + * driver can describe and the request can be repeated.
> + */
> + if (!sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &val))
> + sit9531x_write_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL,
> + val | BIT(SIT9531X_INTSYNC_EN_BIT));
[Severity: Medium]
Does this write need a page-0 small update after it?
The enable and disable sequences both follow their write to
SIT9531X_REG_INTSYNC_GLOBAL with this:
rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
SIT9531X_SMALL_UPDATE_CMD);
That suggests the bit only takes effect once the update runs.
restore_global writes the bit back without one.
Suppose the disable fails after its page-0 update has latched the cleared
bit, for example at the EXT unlock, at any dis_val write, or at the PLL
small update. The active configuration then has INTSYNC off, while the
register reads back as on.
sit9531x_intsync_src_detect() trusts that readback. With a mixed EXT
pattern it records this PLL as the owner, so the SYNCOUT pin reports
CONNECTED while nothing drives the net.
Later, an unrelated priority-table change on any PLL ends in
sit9531x_prio_prg_commit(), which also writes SIT9531X_REG_GLOBAL_UPDATE.
Could that latch the pending bit and turn inter-PLL sync back on over a
half-written EXT page, without anyone requesting it?
The enable rollback goes through the same restore path.
This assumes the global register is a shadow that the small update
latches. The driver's own sequences imply that, but I haven't checked it
against the datasheet.
[ ... ]
> @@ -3379,6 +3711,15 @@ static int sit9531x_dev_state_fetch(struct sit9531x_dev *sitdev)
> return rc;
> }
>
> + mutex_lock(&sitdev->multiop_lock);
> + rc = sit9531x_intsync_src_detect(sitdev);
> + mutex_unlock(&sitdev->multiop_lock);
> + if (rc) {
> + dev_err(sitdev->dev,
> + "Failed to detect INTSYNC source: %d\n", rc);
> + return rc;
> + }
[Severity: Low]
Should INTSYNC ownership also be detected again on resume?
sit9531x_intsync_src_detect() has two callers: this probe-time call and
the SYNCOUT setter. The poll does not call it, and sit9531x_resume()
only does this:
sit9531x_page_cache_drop(sitdev);
if (sitdev->irq > 0)
enable_irq(sitdev->irq);
kthread_queue_delayed_work(sitdev->kworker, &sitdev->work, 0);
The driver already allows for a part that loses power across suspend.
Suppose PLL A was the source, and the device reloads a profile with
INTSYNC disabled or sourced from another PLL. intsync_src stays A.
In that case:
- A's SYNCOUT pin still reports CONNECTED.
- A CONNECTED on A returns success without programming anything.
- A CONNECTED on another PLL fails with -EBUSY.
- A's INTSYNC destination pin is forced to DISCONNECTED.
A DISCONNECTED on A does run the disable and then detect, which refreshes
the cache.
[ ... ]
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index 6bf4efd3c6334..52d5b1a6e36f7 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
[ ... ]
> @@ -772,8 +786,259 @@ sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
[ ... ]
> +static int
> +sit9531x_dpll_intsync_src_state_on_dpll_set(const struct dpll_pin *pin,
[ ... ]
> + if (changed && !rc)
> + sitdev->intsync_src = state == DPLL_PIN_STATE_CONNECTED ?
> + sitdpll->id : -1;
> +
> + /*
> + * Re-scan hardware after a transition so the cache follows a
> + * partially failed enable or disable as closely as possible.
> + */
> + if (changed)
> + detect_rc = sit9531x_intsync_src_detect(sitdev);
[ ... ]
> + mutex_unlock(&sitdev->multiop_lock);
> +
> + if (rc && rc != -EBUSY && rc != -EINVAL && rc != -EOPNOTSUPP)
> + NL_SET_ERR_MSG(extack, "Failed to set INTSYNC source state");
> +
> + return rc;
> +}
[Severity: Medium]
If the request fails but this re-scan moves intsync_src, what sends the
pin change notification?
The core only notifies after a successful set in dpll_pin_state_set().
sit9531x_dpll_changes_check() skips outputs, and that includes the
INTSYNC source:
* Outputs (incl. the INTSYNC source) change only through their
* own set callback and the XO is permanently connected.
On a failed request, the re-scan can still change what the getter
reports. Two examples:
- An enable fails and its rollback disable stops part way. That leaves a
mixed pattern, so detect records this PLL as the partial owner. The pin
goes from DISCONNECTED to CONNECTED.
- A disable fails at its final step. That leaves an all-dis pattern, so
detect sets -1. The pin goes from CONNECTED to DISCONNECTED.
In both cases rc != 0, and no notification is sent.
sit9531x_dpll_output_pin_state_on_dpll_set() already handles this case:
if (rc) {
NL_SET_ERR_MSG(extack, "Failed to set output pin state");
...
if (changed)
__dpll_pin_change_ntf(dpin->dpll_pin);
}
Without the same handling here, won't subscribers keep the stale SYNCOUT
state?
[ ... ]
> +static int
> +sit9531x_dpll_intsync_dst_operstate_on_dpll_get(const struct dpll_pin *pin,
> + void *pin_priv,
> + const struct dpll_device *dpll,
> + void *dpll_priv,
> + enum dpll_pin_operstate *state,
> + struct netlink_ext_ack *extack)
> +{
> + struct sit9531x_dpll *sitdpll = dpll_priv;
> + struct sit9531x_dev *sitdev = sitdpll->dev;
> +
> + mutex_lock(&sitdev->multiop_lock);
> + sit9531x_dpll_selection_operstate_get(sitdev, sitdpll,
> + SIT9531X_INTSYNC_PIN_ID,
> + state);
> + mutex_unlock(&sitdev->multiop_lock);
[Severity: Medium]
Will this report STANDBY even when no PLL drives the net?
sit9531x_dpll_selection_operstate_get() only returns NO_SIGNAL or
QUAL_FAILED for physical inputs:
if (pin_id < sitdev->info->num_inputs) {
ref = sit9531x_ref_state_get(sitdev, pin_id);
if (ref->los) {
*operstate = DPLL_PIN_OPERSTATE_NO_SIGNAL;
...
*operstate = DPLL_PIN_OPERSTATE_STANDBY;
So INTSYNC falls through to STANDBY whenever it is not ACTIVE. The driver
knows when the net is undriven (sitdev->intsync_src < 0), but this getter
doesn't check it.
The dpll uAPI defines standby as "pin is qualified but not actively used
by the DPLL". When the source PLL disconnects, every PLL that lists
INTSYNC loses that reference, yet its INTSYNC pin still reports STANDBY.
sit9531x_dpll_changes_check() uses the same helper, so no notification
goes out either.
The commit message says a destination pin "behaves like any other
selectable input". The state getter comment also compares an undriven net
to "a momentary LOS on an external reference", and for a physical input,
LOS gives NO_SIGNAL.
Should the destination pin report NO_SIGNAL when intsync_src < 0?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260930233714.87679-1-arouhi%40sitime.com
^ permalink raw reply [flat|nested] 26+ messages in thread