* [PATCH v10 01/14] dt-bindings: vendor-prefixes: add SiTime Corporation
2026-09-21 20:11 [PATCH v10 00/14] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
@ 2026-09-21 20:11 ` Ali Rouhi
2026-09-21 20:11 ` [PATCH v10 02/14] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
` (12 subsequent siblings)
13 siblings, 0 replies; 28+ messages in thread
From: Ali Rouhi @ 2026-09-21 20:11 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi,
prabhakar.mahadev-lad.rj, dev, devicetree, netdev, linux-kernel,
Ali Rouhi, Conor Dooley
Add vendor prefix for SiTime Corporation, manufacturer of
programmable clock generators and MEMS oscillators.
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
Acked-by: Conor Dooley <conor.dooley@microchip.com>
---
Notes:
Changes in v10:
No change since v9.
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] 28+ messages in thread* [PATCH v10 02/14] dt-bindings: dpll: add SiTime SiT95316 clock generator
2026-09-21 20:11 [PATCH v10 00/14] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
2026-09-21 20:11 ` [PATCH v10 01/14] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
@ 2026-09-21 20:11 ` Ali Rouhi
2026-09-26 2:34 ` Jakub Kicinski
2026-09-21 20:11 ` [PATCH v10 03/14] dpll: add basic SiTime SiT9531x support Ali Rouhi
` (11 subsequent siblings)
13 siblings, 1 reply; 28+ messages in thread
From: Ali Rouhi @ 2026-09-21 20:11 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi,
prabhakar.mahadev-lad.rj, dev, devicetree, netdev, linux-kernel,
Ali Rouhi
Add a binding for the SiTime SiT9531x family of clock generators: an
I2C-controlled device with four independent PLLs, up to eight input
clocks and up to twelve outputs, described as a DPLL provider.
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
Notes:
Changes in v10:
Bounded dpll-types to the four PLLs the part has.
Made the example self-consistent with the driver: the Fvco override is
on a PLL whose band the value belongs to, and is a rate the example's
own output pin divides down from exactly.
Dropped esync-control from the example, along with the
embedded-sync patch it belonged to.
.../bindings/dpll/sitime,sit95316.yaml | 183 ++++++++++++++++++
MAINTAINERS | 6 +
2 files changed, 189 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..6f4c60ba1cb9
--- /dev/null
+++ b/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
@@ -0,0 +1,183 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/dpll/sitime,sit95316.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: SiTime SiT95316/SiT95317 DPLL Clock Generator
+
+maintainers:
+ - Ali Rouhi <arouhi@sitime.com>
+
+description: |
+ SiTime SiT95316 and SiT95317 are I2C-controlled programmable clock
+ generators with integrated DPLL for synchronization applications. Both
+ variants contain four PLLs with automatic/manual reference selection,
+ DCO frequency adjustment, and phase offset measurement via an on-chip
+ TDC (Time-to-Digital Converter).
+
+ Both parts have 4 differential input pairs whose lanes can also be
+ driven independently as single-ended references, so 8 inputs are
+ individually selectable. SiT95317 drives 8 outputs, SiT95316
+ drives 12.
+
+properties:
+ compatible:
+ enum:
+ - sitime,sit95316
+ - sitime,sit95317
+
+ reg:
+ maxItems: 1
+
+ clocks:
+ maxItems: 1
+
+ clock-names:
+ items:
+ - const: xtal
+
+ clock-frequency:
+ description:
+ XO rate in Hz feeding XIN/XO_CLK. Alternative to the "clocks"
+ phandle for platforms where the firmware does not expose the
+ oscillator through the clock framework.
+
+ reset-gpios:
+ maxItems: 1
+ description:
+ GPIO connected to the chip's active-low reset pin (RESETB).
+
+ interrupts:
+ maxItems: 1
+ description:
+ Interrupt from the chip's active-low INTRB output. Asserted when
+ the device detects a status change such as lock acquisition or loss.
+
+ dpll-types:
+ maxItems: 4
+
+ sitime,pll-fvco:
+ $ref: /schemas/types.yaml#/definitions/uint64-array
+ minItems: 4
+ maxItems: 4
+ description:
+ Per-PLL VCO frequency in Hz for PLLA, PLLB, PLLC, PLLD. The
+ values exceed 32 bits, which requires an explicit uint64-array
+ type; the dtschema meta-schema does not permit a type $ref on a
+ "-hz" unit-suffix property (only opp-hz is defined as 64-bit),
+ hence no unit suffix. Override where the standard
+ Fvco = Fref * DIVN derivation does not match the running VCO --
+ for example a PLL operating in INTSYNC mode. A value of 0 keeps
+ the register-derived computation for that PLL.
+
+ sitime,output-pll-map:
+ $ref: /schemas/types.yaml#/definitions/uint32-array
+ minItems: 8
+ maxItems: 12
+ items:
+ enum: [0, 1, 2, 3, 255]
+ description:
+ Source PLL index (0=PLLA .. 3=PLLD) for each output 0..11. The
+ value 255 (0xff) marks an output as unmapped and prevents the
+ driver from registering it as a DPLL pin. Override for
+ configurations where the chip's per-PLL OUTPUT_ENABLE bitmaps do
+ not unambiguously describe output-to-PLL routing.
+
+required:
+ - compatible
+ - reg
+
+oneOf:
+ - required:
+ - clocks
+ - clock-names
+ - required:
+ - clock-frequency
+
+allOf:
+ - $ref: /schemas/dpll/dpll-device.yaml#
+ # SiT95317 exposes 8 outputs, SiT95316 exposes 12. Bound the
+ # output-pll-map length to the variant so a SiT95317 node cannot
+ # describe more outputs than the part has.
+ - if:
+ properties:
+ compatible:
+ contains:
+ const: sitime,sit95317
+ then:
+ properties:
+ sitime,output-pll-map:
+ maxItems: 8
+ # SiT95316 has 12 outputs. When the map is supplied it must describe
+ # all of them, otherwise the trailing outputs are left ambiguous.
+ - if:
+ properties:
+ compatible:
+ contains:
+ const: sitime,sit95316
+ then:
+ properties:
+ sitime,output-pll-map:
+ minItems: 12
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ dpll@68 {
+ compatible = "sitime,sit95316";
+ reg = <0x68>;
+ clocks = <&xo2>;
+ clock-names = "xtal";
+ reset-gpios = <&gpio 78 GPIO_ACTIVE_LOW>;
+ interrupts = <12 IRQ_TYPE_LEVEL_LOW>;
+ dpll-types = "eec", "eec", "eec", "eec";
+ sitime,pll-fvco = /bits/ 64 <0 6875000000 0 0>;
+ sitime,output-pll-map = <0 0 0 0 0 0 0 0 0 0 0 0>;
+
+ input-pins {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ pin@0 {
+ reg = <0>;
+ label = "clkin0";
+ connection-type = "ext";
+ supported-frequencies-hz = /bits/ 64 <10000000>;
+ };
+
+ pin@1 {
+ reg = <1>;
+ label = "clkin1";
+ connection-type = "synce";
+ supported-frequencies-hz = /bits/ 64 <156250000>;
+ };
+ };
+
+ output-pins {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ pin@0 {
+ reg = <0>;
+ label = "clkout0";
+ supported-frequencies-hz = /bits/ 64 <156250000>;
+ };
+
+ pin@1 {
+ reg = <1>;
+ label = "clkout1";
+ supported-frequencies-hz = /bits/ 64 <25000000>;
+ };
+ };
+ };
+ };
+...
diff --git a/MAINTAINERS b/MAINTAINERS
index 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] 28+ messages in thread* Re: [PATCH v10 02/14] dt-bindings: dpll: add SiTime SiT95316 clock generator
2026-09-21 20:11 ` [PATCH v10 02/14] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
@ 2026-09-26 2:34 ` Jakub Kicinski
0 siblings, 0 replies; 28+ messages in thread
From: Jakub Kicinski @ 2026-09-26 2:34 UTC (permalink / raw)
To: arouhi
Cc: Jakub Kicinski, jiri, vadim.fedorenko, arkadiusz.kubalewski,
ivecera, robh, krzk+dt, conor+dt, cjubran, pabeni,
Oleg.Zadorozhnyi, prabhakar.mahadev-lad.rj, dev, devicetree,
netdev, linux-kernel
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
dt-bindings: dpll: add SiTime SiT95316 clock generator
This adds a devicetree binding for the SiTime SiT95316/SiT95317 DPLL clock
generators. These are I2C devices with four PLLs, up to eight inputs and
up to twelve outputs. The binding builds on the common dpll-device schema,
adds vendor properties for per-PLL VCO overrides and output-to-PLL routing,
and adds a MAINTAINERS entry.
> diff --git a/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml b/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
> new file mode 100644
> index 0000000000000..6f4c60ba1cb92
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
> @@ -0,0 +1,183 @@
[ ... ]
> + clock-frequency:
> + description:
> + XO rate in Hz feeding XIN/XO_CLK. Alternative to the "clocks"
> + phandle for platforms where the firmware does not expose the
> + oscillator through the clock framework.
> +
[ ... ]
> + sitime,pll-fvco:
> + $ref: /schemas/types.yaml#/definitions/uint64-array
> + minItems: 4
> + maxItems: 4
> + description:
> + Per-PLL VCO frequency in Hz for PLLA, PLLB, PLLC, PLLD. The
> + values exceed 32 bits, which requires an explicit uint64-array
> + type; the dtschema meta-schema does not permit a type $ref on a
> + "-hz" unit-suffix property (only opp-hz is defined as 64-bit),
> + hence no unit suffix. Override where the standard
> + Fvco = Fref * DIVN derivation does not match the running VCO --
> + for example a PLL operating in INTSYNC mode. A value of 0 keeps
> + the register-derived computation for that PLL.
> +
> + sitime,output-pll-map:
> + $ref: /schemas/types.yaml#/definitions/uint32-array
> + minItems: 8
> + maxItems: 12
> + items:
> + enum: [0, 1, 2, 3, 255]
> + description:
> + Source PLL index (0=PLLA .. 3=PLLD) for each output 0..11. The
> + value 255 (0xff) marks an output as unmapped and prevents the
> + driver from registering it as a DPLL pin. Override for
> + configurations where the chip's per-PLL OUTPUT_ENABLE bitmaps do
> + not unambiguously describe output-to-PLL routing.
[Severity: Medium]
Do clock-frequency, sitime,pll-fvco and sitime,output-pll-map describe
the hardware, or what the Linux driver and clock framework do today?
Documentation/devicetree/bindings/writing-bindings.rst says:
DON'T refer to Linux or "device driver" in bindings. Bindings should be
based on what the hardware has, not what an OS and driver currently
support.
In sitime,output-pll-map, the value 255 "prevents the driver from
registering it as a DPLL pin". The property exists to override the chip's
own OUTPUT_ENABLE routing bitmaps.
In sitime,pll-fvco, a value of 0 "keeps the register-derived
computation". The property overrides cases where the driver's
Fvco = Fref * DIVN derivation is wrong.
The reason given for clock-frequency is that the firmware does not expose
the oscillator through the clock framework. A fixed-clock node can already
describe the XO, so this adds a second, mutually exclusive ABI for the
same thing.
Both vendor properties copy state that the chip already holds in its
programmed configuration, and they would become permanent DT ABI.
Can a static sitime,pll-fvco value stay correct at runtime? Later in the
series, "dpll: sit9531x: model the inter-PLL sync net as a pair of pins"
lets userspace switch INTSYNC through netlink. The sync source pin
state_set in drivers/dpll/sit9531x/dpll.c does:
sitdev->intsync_src = sitdpll->id;
(or -1). sit9531x_get_fvco() in drivers/dpll/sit9531x/core.c returns the
override whatever the INTSYNC state is:
if (pll_idx < SIT9531X_NUM_PLLS && sitdev->pll_fvco[pll_idx]) {
*fvco = sitdev->pll_fvco[pll_idx];
return 0;
}
If the VCO rate really depends on INTSYNC, which is the reason this
binding gives for the override, would the value be stale after a runtime
switch? sit9531x_output_divo_calc() and the phase and TDC paths all divide
by sit9531x_get_fvco(). A stale value would give wrong output frequencies,
phase offsets and DIVO values. I couldn't confirm whether the VCO actually
changes with INTSYNC on this hardware.
The reasons given also don't match. This binding cites INTSYNC mode. The
later commit "dpll: sit9531x: allow the device tree to override two board
facts" cites "free-run with a divider the configuration never programmed".
Which case is the override meant for?
The commit message doesn't mention these vendor properties or the
clock-frequency alternative. Could it explain why they are needed?
> +
> +required:
> + - compatible
> + - reg
> +
> +oneOf:
> + - required:
> + - clocks
> + - clock-names
> + - required:
> + - clock-frequency
[Severity: Low]
Is a node that has both clocks and clock-frequency supposed to pass this
oneOf?
Take a node with clocks = <&xo>; and clock-frequency = <...>; but no
clock-names. The first branch fails only because clock-names is missing,
and the second branch passes, so oneOf is satisfied. Nothing else in the
binding requires clock-names when clocks is present.
For such a node, sit9531x_dev_probe() does:
xtal_clk = devm_clk_get_optional_enabled(sitdev->dev, "xtal");
...
if (!device_property_read_u32(sitdev->dev, "clock-frequency",
&freq))
The "xtal" lookup finds no clock-names match, so the optional getter
returns NULL. clk_get_rate(NULL) is 0, and the driver falls back to
clock-frequency. The clock referenced by clocks is never prepared or
enabled.
Would adding dependentRequired (clocks requires clock-names), or having
each oneOf branch forbid the other property, fix this?
> +
> +allOf:
> + - $ref: /schemas/dpll/dpll-device.yaml#
[Severity: Low]
This isn't a bug introduced by this patch, but the new binding inherits a
limitation from the shared schema. In dpll-device.yaml, input-pins
children match:
"^pin@[0-9a-f]+$":
output-pins children match:
"^pin@[0-9]+$":
output-pins also has unevaluatedProperties: false, so pin@a and pin@b are
rejected.
The SiT95316 has outputs 0..11, and the driver matches output-pins
children by physical slot. On the SiT95317 the slots are:
static const u8 clkout_map_95317[] = {0, 3, 4, 5, 7, 8, 9, 11};
so OUT11 is reg 11 on both parts.
Can outputs 10 and 11 be described with the usual hex unit-address names?
The only way to pass validation seems to be pin@10 with reg = <10>, which
breaks the hex convention. microchip,zl30731.yaml (20 single-ended outputs)
has the same gap. Should the output-pins pattern in dpll-device.yaml be
changed to match the input-pins one?
> + # SiT95317 exposes 8 outputs, SiT95316 exposes 12. Bound the
> + # output-pll-map length to the variant so a SiT95317 node cannot
> + # describe more outputs than the part has.
> + - if:
> + properties:
> + compatible:
> + contains:
> + const: sitime,sit95317
> + then:
> + properties:
> + sitime,output-pll-map:
> + maxItems: 8
> + # SiT95316 has 12 outputs. When the map is supplied it must describe
> + # all of them, otherwise the trailing outputs are left ambiguous.
> + - if:
> + properties:
> + compatible:
> + contains:
> + const: sitime,sit95316
> + then:
> + properties:
> + sitime,output-pll-map:
> + minItems: 12
[Severity: Medium]
What do the reg values of input-pins and output-pins children mean on
these parts, and which values are valid for each variant?
These if/then blocks only limit sitime,output-pll-map. dpll-pin.yaml only
has reg: maxItems: 1, so nothing here limits pin reg values.
sit9531x_prop_pin_fwnode_get() in drivers/dpll/sit9531x/prop.c matches
output-pins children by physical slot:
if (id < sitdev->info->num_outputs)
id = sitdev->info->clkout_map[id];
On the SiT95317 this puts the eight outputs at reg 0, 3, 4, 5, 7, 8, 9 and
11. sitime,output-pll-map uses the dense logical index 0..7 instead. The
binding only says "SiT95317 drives 8 outputs" and "for each output
0..11".
Suppose a SiT95317 board, going by the binding text, numbers its output
pins 0..7. Wouldn't reg 3 then apply to logical output 1, reg 4 to logical
2, reg 5 to logical 3, and reg 7 to logical 4, with reg 1, 2 and 6
ignored? Labels, connection types and supported-frequencies-hz would end
up on the wrong outputs. Output reg 1, 2, 6 or 10, and input reg 8 or
higher, also pass validation and are silently ignored.
The binding doesn't describe the input lane layout either. In
drivers/dpll/sit9531x/core.h:
static inline bool sit9531x_input_is_n(u8 index)
{
return index & 1;
}
So reg 2k is CLKINkP and reg 2k+1 is CLKINkN. When the pair is
differential, sit9531x_input_pin_is_registrable() doesn't register the N
lane at all.
In the example, input pin@0 (10 MHz, ext) and pin@1 (156.25 MHz, synce)
are the two lanes of CLKIN0. That only works if the pair is set up as
single-ended.
Could the binding document the reg numbering and add reg limits for each
variant? microchip,zl30731.yaml labels each example pin with its physical
name (/* REF0P */), which could be a model to follow.
> +
> +unevaluatedProperties: false
[ ... ]
--
pw-bot: cr
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH v10 03/14] dpll: add basic SiTime SiT9531x support
2026-09-21 20:11 [PATCH v10 00/14] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
2026-09-21 20:11 ` [PATCH v10 01/14] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
2026-09-21 20:11 ` [PATCH v10 02/14] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
@ 2026-09-21 20:11 ` Ali Rouhi
2026-09-26 2:34 ` Jakub Kicinski
2026-09-21 20:11 ` [PATCH v10 04/14] dpll: sit9531x: read DPLL types and pin properties from system firmware Ali Rouhi
` (10 subsequent siblings)
13 siblings, 1 reply; 28+ messages in thread
From: Ali Rouhi @ 2026-09-21 20:11 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi,
prabhakar.mahadev-lad.rj, dev, devicetree, netdev, linux-kernel,
Ali Rouhi
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
The SiT9531x is an I2C clock generator with four independent PLLs, up to
eight input clocks and up to twelve outputs. Later patches register the
PLLs with the DPLL subsystem and expose the clocks as pins; this one adds
only what is needed to reach the device.
The register space is paged: 32 pages of 256 registers, selected by
writing the page number to offset 0xFF, which is present in every page.
A regmap range configuration describes that window, so the rest of the
driver addresses a register as a page and an offset and never touches the
selector itself. Pages come in pairs for the PLLs (0x0A/0x1A for PLLA,
and so on).
Probe reads the rate of the crystal feeding XIN, since every frequency
the driver later computes derives from it; takes the optional reset line
and leaves it deasserted, because the device configuration comes from
efuse or from a blob applied before probe and a reset would discard it;
then identifies the variant from the single byte at page 0 offset 0x02
and refuses to bind on anything unknown.
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>
---
Notes:
Changes in v10:
Cached the page selector, which Ivan Vecera suggested. It is the only
register in the map that is not volatile, which removes a selector
write per register access on a paged device.
Moved "select DPLL" and the paragraph about registering DPLL devices to
the patch that does that; at this commit nothing here includes a DPLL
header.
Brought the added lines inside eighty columns, the width the CI checks.
MAINTAINERS | 1 +
drivers/dpll/Kconfig | 2 +
drivers/dpll/Makefile | 1 +
drivers/dpll/sit9531x/Kconfig | 12 ++
drivers/dpll/sit9531x/Makefile | 4 +
drivers/dpll/sit9531x/core.c | 288 +++++++++++++++++++++++++++++++++
drivers/dpll/sit9531x/core.h | 91 +++++++++++
drivers/dpll/sit9531x/regs.h | 44 +++++
8 files changed, 443 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..c306d1dd1c58
--- /dev/null
+++ b/drivers/dpll/sit9531x/Kconfig
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+config SIT9531X_DPLL
+ tristate "SiTime SiT9531x DPLL driver"
+ depends on I2C && NET
+ 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..cf1cda87fa52
--- /dev/null
+++ b/drivers/dpll/sit9531x/core.c
@@ -0,0 +1,288 @@
+// 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, which only
+ * this driver moves. 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.
+ */
+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,
+};
+
+/*
+ * sit9531x_read_u8 - read an 8-bit register
+ * @reg: register in SIT9531X_REG(page, offset) form
+ * @val: output value
+ */
+int sit9531x_read_u8(struct sit9531x_dev *sitdev, unsigned int reg,
+ u8 *val)
+{
+ unsigned int tmp;
+ int rc;
+
+ reg = (SIT9531X_REG_PAGE(reg) * SIT9531X_PAGE_SIZE) +
+ SIT9531X_REG_OFFSET(reg) + SIT9531X_RANGE_OFFSET;
+
+ rc = regmap_read(sitdev->regmap, reg, &tmp);
+ if (rc)
+ dev_err(sitdev->dev, "Failed to read reg 0x%04x: %d\n",
+ reg, rc);
+ else
+ *val = (u8)tmp;
+
+ return rc;
+}
+
+/*
+ * sit9531x_write_u8 - write an 8-bit register
+ * @reg: register in SIT9531X_REG(page, offset) form
+ * @val: value to write
+ */
+int sit9531x_write_u8(struct sit9531x_dev *sitdev, unsigned int reg,
+ u8 val)
+{
+ int rc;
+
+ reg = (SIT9531X_REG_PAGE(reg) * SIT9531X_PAGE_SIZE) +
+ SIT9531X_REG_OFFSET(reg) + SIT9531X_RANGE_OFFSET;
+
+ rc = regmap_write(sitdev->regmap, reg, val);
+ if (rc)
+ dev_err(sitdev->dev, "Failed to write reg 0x%04x: %d\n",
+ reg, rc);
+
+ return rc;
+}
+
+/*
+ * sit9531x_read_pll_u8 - read a register on a PLL page
+ * @val: output value
+ */
+int sit9531x_read_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 offset, u8 *val)
+{
+ 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)
+{
+ unsigned int reg;
+
+ reg = (sit9531x_pll_page(pll_idx) * SIT9531X_PAGE_SIZE) +
+ offset + SIT9531X_RANGE_OFFSET;
+
+ return regmap_update_bits(sitdev->regmap, reg, mask, val);
+}
+
+static int sit9531x_read_variant_id(struct sit9531x_dev *sitdev, u8 *id)
+{
+ return sit9531x_read_u8(sitdev, SIT9531X_REG_VARIANT_ID, id);
+}
+
+static const struct sit9531x_chip_info *sit9531x_match_variant(u8 id)
+{
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(sit9531x_chip_ids); i++) {
+ if (sit9531x_chip_ids[i].id == id)
+ return &sit9531x_chip_ids[i];
+ }
+
+ return NULL;
+}
+
+int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
+{
+ struct clk *xtal_clk;
+ u8 variant_id;
+ int rc;
+
+ /*
+ * Fvco = Fref * (DIVN + frac/2^32) with Fref derived from the XO
+ * feeding XIN/XO_CLK, so the rate is needed before anything can be
+ * computed from a divider. The rate normally comes from a "clocks"
+ * phandle (clock-names = "xtal"). On platforms where the firmware
+ * does not expose the XO through the clock framework, fall back to
+ * a "clock-frequency" device property.
+ */
+ xtal_clk = devm_clk_get_optional_enabled(sitdev->dev, "xtal");
+ if (IS_ERR(xtal_clk))
+ return dev_err_probe(sitdev->dev, PTR_ERR(xtal_clk),
+ "Failed to get xtal clock\n");
+ sitdev->xtal_freq = clk_get_rate(xtal_clk);
+ if (!sitdev->xtal_freq) {
+ u32 freq;
+
+ if (!device_property_read_u32(sitdev->dev, "clock-frequency",
+ &freq))
+ sitdev->xtal_freq = freq;
+ }
+ if (!sitdev->xtal_freq)
+ return dev_err_probe(sitdev->dev, -EINVAL,
+ "no xtal rate: provide clocks=<&xo> + clock-names=\"xtal\", or a clock-frequency property\n");
+
+ /*
+ * Held deasserted, never pulsed: the chip configuration comes from
+ * efuse or an NVM blob applied before probe, and a reset would
+ * discard it. Must precede the first I2C access, as a board that
+ * powers up asserted keeps the chip unreachable until released.
+ */
+ sitdev->reset_gpio = devm_gpiod_get_optional(sitdev->dev, "reset",
+ GPIOD_OUT_LOW);
+ if (IS_ERR(sitdev->reset_gpio))
+ return dev_err_probe(sitdev->dev, PTR_ERR(sitdev->reset_gpio),
+ "Failed to request reset gpio\n");
+ if (sitdev->reset_gpio)
+ fsleep(10000); /* internal boot after release */
+
+ rc = sit9531x_read_variant_id(sitdev, &variant_id);
+ if (rc)
+ return rc;
+
+ sitdev->info = sit9531x_match_variant(variant_id);
+ if (!sitdev->info)
+ return dev_err_probe(sitdev->dev, -ENODEV,
+ "Unknown variant ID: 0x%02x\n",
+ variant_id);
+
+ rc = devm_mutex_init(sitdev->dev, &sitdev->multiop_lock);
+ if (rc)
+ return dev_err_probe(sitdev->dev, rc,
+ "Failed to initialize mutex\n");
+
+ dev_info(sitdev->dev, "%s detected, %u inputs, %u outputs\n",
+ sitdev->info->name, sitdev->info->num_inputs,
+ sitdev->info->num_outputs);
+
+ return 0;
+}
+
+static int sit9531x_i2c_probe(struct i2c_client *client)
+{
+ struct sit9531x_dev *sitdev;
+ struct regmap *regmap;
+
+ regmap = devm_regmap_init_i2c(client, &sit9531x_regmap_config);
+ if (IS_ERR(regmap))
+ return dev_err_probe(&client->dev, PTR_ERR(regmap),
+ "Failed to initialize regmap\n");
+
+ sitdev = devm_kzalloc(&client->dev, sizeof(*sitdev), GFP_KERNEL);
+ if (!sitdev)
+ return -ENOMEM;
+
+ sitdev->dev = &client->dev;
+ sitdev->client = client;
+ sitdev->regmap = regmap;
+ i2c_set_clientdata(client, sitdev);
+
+ return sit9531x_dev_probe(sitdev);
+}
+
+static const struct of_device_id sit9531x_of_match[] = {
+ { .compatible = "sitime,sit95316" },
+ { .compatible = "sitime,sit95317" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, sit9531x_of_match);
+
+static struct i2c_driver sit9531x_i2c_driver = {
+ .driver = {
+ .name = "sit9531x",
+ .of_match_table = sit9531x_of_match,
+ },
+ .probe = sit9531x_i2c_probe,
+};
+module_i2c_driver(sit9531x_i2c_driver);
+
+MODULE_AUTHOR("Ali Rouhi <arouhi@sitime.com>");
+MODULE_AUTHOR("Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>");
+MODULE_DESCRIPTION("SiTime SiT9531x DPLL subsystem driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
new file mode 100644
index 000000000000..76a2632f0ce4
--- /dev/null
+++ b/drivers/dpll/sit9531x/core.h
@@ -0,0 +1,91 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * SiTime SiT9531x DPLL core driver
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * Device structure, register access helpers, and core function
+ * declarations.
+ */
+
+#ifndef _SIT9531X_CORE_H
+#define _SIT9531X_CORE_H
+
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/mutex.h>
+#include <linux/regmap.h>
+#include <linux/types.h>
+
+#include "regs.h"
+
+#define SIT9531X_NUM_PLLS 4
+#define SIT9531X_MAX_INPUTS 8
+#define SIT9531X_MAX_OUTPUTS 12
+
+/*
+ * struct sit9531x_chip_info - chip variant identification
+ * @id: variant ID byte read from register
+ * @num_inputs: number of input clock pins
+ * @num_outputs: number of output clock pins
+ * @name: human-readable variant name
+ * @clkout_map: per-output slot mapping (output index -> physical slot)
+ */
+struct sit9531x_chip_info {
+ u8 id;
+ u8 num_inputs;
+ u8 num_outputs;
+ const char *name;
+ const u8 *clkout_map;
+};
+
+/*
+ * struct sit9531x_dev - SiT9531x device instance
+ * @dev: parent device
+ * @client: I2C client
+ * @regmap: paged register map
+ * @info: detected chip variant info
+ * @multiop_lock: serializes multi-register sequences
+ * @xtal_freq: crystal oscillator frequency in Hz
+ * @reset_gpio: optional reset line (DT "reset-gpios"), NULL if absent
+ */
+struct sit9531x_dev {
+ struct device *dev;
+ struct i2c_client *client;
+ struct regmap *regmap;
+ const struct sit9531x_chip_info *info;
+ /* Serializes multi-step register sequences */
+ struct mutex multiop_lock;
+
+ u32 xtal_freq;
+
+ struct gpio_desc *reset_gpio;
+};
+
+/*
+ * sit9531x_pll_page - get register page for PLL index
+ * @pll_idx: PLL index (0 = PLLA, 3 = PLLD)
+ */
+static inline u8 sit9531x_pll_page(u8 pll_idx)
+{
+ return SIT9531X_PAGE_PLLA + pll_idx;
+}
+
+extern const struct regmap_config sit9531x_regmap_config;
+
+/* ---- Core lifecycle ---- */
+int sit9531x_dev_probe(struct sit9531x_dev *sitdev);
+
+/* ---- Register access ---- */
+int sit9531x_read_u8(struct sit9531x_dev *sitdev, unsigned int reg, u8 *val);
+int sit9531x_write_u8(struct sit9531x_dev *sitdev, unsigned int reg, u8 val);
+int sit9531x_read_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx, u8 offset,
+ u8 *val);
+int sit9531x_write_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx, u8 offset,
+ u8 val);
+int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx, u8 offset,
+ u8 mask, u8 val);
+
+#endif /* _SIT9531X_CORE_H */
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
new file mode 100644
index 000000000000..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] 28+ messages in thread* Re: [PATCH v10 03/14] dpll: add basic SiTime SiT9531x support
2026-09-21 20:11 ` [PATCH v10 03/14] dpll: add basic SiTime SiT9531x support Ali Rouhi
@ 2026-09-26 2:34 ` Jakub Kicinski
0 siblings, 0 replies; 28+ messages in thread
From: Jakub Kicinski @ 2026-09-26 2:34 UTC (permalink / raw)
To: arouhi
Cc: Jakub Kicinski, jiri, vadim.fedorenko, arkadiusz.kubalewski,
ivecera, robh, krzk+dt, conor+dt, cjubran, pabeni,
Oleg.Zadorozhnyi, prabhakar.mahadev-lad.rj, dev, devicetree,
netdev, linux-kernel
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
dpll: add basic SiTime SiT9531x support
This commit adds a basic I2C driver for the SiTime SiT9531x clock
generators. It uses a paged regmap range configuration and adds register
access helpers. Probe gets the xtal rate, takes the optional reset GPIO and
identifies the variant from page 0 offset 0x02.
> diff --git a/drivers/dpll/sit9531x/Kconfig b/drivers/dpll/sit9531x/Kconfig
> new file mode 100644
> index 0000000000000..c306d1dd1c58a
> --- /dev/null
> +++ b/drivers/dpll/sit9531x/Kconfig
> @@ -0,0 +1,12 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +
> +config SIT9531X_DPLL
> + tristate "SiTime SiT9531x DPLL driver"
> + depends on I2C && NET
> + select REGMAP_I2C
[Severity: Low]
At this commit, can SIT9531X_DPLL build anything unless some other driver
selects DPLL?
The only way kbuild enters drivers/dpll/ is through drivers/Makefile:
obj-$(CONFIG_DPLL) += dpll/
DPLL is a hidden bool that has no prompt. If SIT9531X_DPLL is the only DPLL
user in a config, the line
obj-$(CONFIG_SIT9531X_DPLL) += sit9531x/
is never evaluated, and no sit9531x object or module gets built.
A later commit in the series, "dpll: sit9531x: register DPLL devices and
pins", adds select DPLL, and the v10 notes say the move is deliberate. So
this only affects build coverage and bisection at this intermediate commit.
[ ... ]
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> new file mode 100644
> index 0000000000000..cf1cda87fa52c
> --- /dev/null
> +++ b/drivers/dpll/sit9531x/core.c
[ ... ]
> +/*
> + * Everything the device holds can change without the driver writing it,
> + * so nothing here is cacheable -- except the page selector, which only
> + * this driver moves. 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.
> + */
> +static bool sit9531x_volatile_reg(struct device *dev __maybe_unused,
> + unsigned int reg)
> +{
> + return reg != SIT9531X_PAGE_SEL;
> +}
[ ... ]
> +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,
> +};
[Severity: Medium]
Can the cached page selector get out of sync with the chip, and then stay
that way?
_regmap_select_page() changes pages with _regmap_update_bits() on
selector_reg. Since 0xFF is non-volatile, the old value comes from the
cache, and the write is skipped when that cached value already matches:
drivers/base/regmap/regmap.c:_regmap_update_bits() {
...
ret = _regmap_read(map, reg, &orig);
...
if (force_write || (tmp != orig) || map->force_write_field) {
ret = _regmap_write(map, reg, tmp);
...
}
On adapters that only support SMBus byte data, devm_regmap_init_i2c() uses
the regmap_smbus_byte bus. On that bus, _regmap_write() updates the cache
before it does the bus write:
drivers/base/regmap/regmap.c:_regmap_write() {
...
ret = regcache_write(map, reg, val);
...
ret = map->reg_write(context, reg, val);
...
}
Suppose i2c_smbus_write_byte_data() fails for the selector write. The error
comes back, but the cached value is not dropped. The raw write path in
_regmap_raw_write_impl() does drop it with map->cache_ops->drop(). The chip
then stays on page A while the cache says page B.
Would the next access to page B skip the selector write? It would then
read or write the same offset on page A without returning an error. That
could be another PLL's page (0x0A-0x0D/0x1A-0x1D) or one of the
output/system pages.
On error, sit9531x_read_u8() and sit9531x_write_u8() only log and return
rc. Nothing in the series calls regcache_drop_region(),
regcache_mark_dirty() or regcache_cache_bypass(). The sit9531x_resume()
added later only re-queues the poll worker.
Would a selector reset on the chip side also go uncorrected, for example
power loss across suspend or an internal reload?
Before v10 the selector was volatile, so a stale value was fixed on the
next access. The missing drop on the SMBus path is in regmap core, but
caching the selector here is what exposes it. The zl3073x driver uses a
similar pattern.
The comment says the selector is something "only this driver moves". Does
that still hold once a transfer fails or the chip resets?
> +/*
> + * sit9531x_read_u8 - read an 8-bit register
> + * @reg: register in SIT9531X_REG(page, offset) form
> + * @val: output value
> + */
> +int sit9531x_read_u8(struct sit9531x_dev *sitdev, unsigned int reg,
> + u8 *val)
> +{
> + unsigned int tmp;
> + int rc;
> +
> + reg = (SIT9531X_REG_PAGE(reg) * SIT9531X_PAGE_SIZE) +
> + SIT9531X_REG_OFFSET(reg) + SIT9531X_RANGE_OFFSET;
> +
> + rc = regmap_read(sitdev->regmap, reg, &tmp);
> + if (rc)
> + dev_err(sitdev->dev, "Failed to read reg 0x%04x: %d\n",
> + reg, rc);
[Severity: Low]
This isn't a bug, but is this message meant to print the regmap virtual
address rather than the SIT9531X_REG(page, offset) value the caller passed
in?
reg has already been rewritten at this point. Since SIT9531X_RANGE_OFFSET
equals SIT9531X_PAGE_SIZE, the printed value is exactly one page above the
regs.h encoding.
For example, a failed VARIANT_ID read, SIT9531X_REG(0x00, 0x02) = 0x0002,
is logged as "reg 0x0102", which looks like page 1, offset 0x02.
Would it make sense to keep the original value for the message? The same
applies to "Failed to write reg" in sit9531x_write_u8() below.
> + else
> + *val = (u8)tmp;
> +
> + return rc;
> +}
> +
> +/*
> + * sit9531x_write_u8 - write an 8-bit register
> + * @reg: register in SIT9531X_REG(page, offset) form
> + * @val: value to write
> + */
> +int sit9531x_write_u8(struct sit9531x_dev *sitdev, unsigned int reg,
> + u8 val)
> +{
> + int rc;
> +
> + reg = (SIT9531X_REG_PAGE(reg) * SIT9531X_PAGE_SIZE) +
> + SIT9531X_REG_OFFSET(reg) + SIT9531X_RANGE_OFFSET;
> +
> + rc = regmap_write(sitdev->regmap, reg, val);
> + if (rc)
> + dev_err(sitdev->dev, "Failed to write reg 0x%04x: %d\n",
> + reg, rc);
[ ... ]
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH v10 04/14] dpll: sit9531x: read DPLL types and pin properties from system firmware
2026-09-21 20:11 [PATCH v10 00/14] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (2 preceding siblings ...)
2026-09-21 20:11 ` [PATCH v10 03/14] dpll: add basic SiTime SiT9531x support Ali Rouhi
@ 2026-09-21 20:11 ` Ali Rouhi
2026-09-26 2:34 ` Jakub Kicinski
2026-09-21 20:11 ` [PATCH v10 05/14] dpll: sit9531x: register DPLL devices and pins Ali Rouhi
` (9 subsequent siblings)
13 siblings, 1 reply; 28+ messages in thread
From: Ali Rouhi @ 2026-09-21 20:11 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi,
prabhakar.mahadev-lad.rj, dev, devicetree, netdev, linux-kernel,
Ali Rouhi
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
The DPLL core wants a type per device and a property set per pin: a
label, the direction, the capabilities, and the frequencies the pin
supports. None of that can be read from the chip -- which frequencies a
board actually presents on a given input 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.
Two of the advertised properties are worth naming. Outputs get a
phase-adjust window of 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. And 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>
---
Notes:
Changes in v10:
Named and matched output pins by the chip pin they drive rather than
by the driver's index for them, which differ on the variant whose
outputs are bonded out from a subset of the slots.
Took an output's current rate from its divider instead of the first
entry of the list of rates the board supports.
Advertised the phase-adjust window and the priority capability from the
patches that implement them.
Described the advertised phase window and the fallback frequency range
in the commit message.
Dropped the esync-control property, since the patch that read
it has been withdrawn.
drivers/dpll/sit9531x/Makefile | 2 +-
drivers/dpll/sit9531x/core.h | 72 ++++++
drivers/dpll/sit9531x/prop.c | 430 +++++++++++++++++++++++++++++++++
drivers/dpll/sit9531x/prop.h | 37 +++
4 files changed, 540 insertions(+), 1 deletion(-)
create mode 100644 drivers/dpll/sit9531x/prop.c
create mode 100644 drivers/dpll/sit9531x/prop.h
diff --git a/drivers/dpll/sit9531x/Makefile b/drivers/dpll/sit9531x/Makefile
index a221fe55386a..819af61123f5 100644
--- a/drivers/dpll/sit9531x/Makefile
+++ b/drivers/dpll/sit9531x/Makefile
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_SIT9531X_DPLL) += sit9531x.o
-sit9531x-y := core.o
+sit9531x-y := core.o prop.o
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 76a2632f0ce4..230b21b9e238 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -23,7 +23,17 @@
#define SIT9531X_NUM_PLLS 4
#define SIT9531X_MAX_INPUTS 8
+#define SIT9531X_NUM_INPUT_PAIRS (SIT9531X_MAX_INPUTS / 2)
#define SIT9531X_MAX_OUTPUTS 12
+/*
+ * INTSYNC (the inter-PLL sync net) is modeled as two pins. The
+ * destination PLL that locks to INTSYNC sees an input pin
+ * (SIT9531X_INTSYNC_PIN_ID, in the input id namespace after the physical
+ * inputs and the xtal); the source PLL that drives INTSYNC sees an output
+ * pin (SIT9531X_INTSYNC_OUT_PIN_ID, appended after the physical outputs).
+ */
+#define SIT9531X_INTSYNC_PIN_ID (SIT9531X_MAX_INPUTS + 1)
+#define SIT9531X_INTSYNC_OUT_PIN_ID SIT9531X_MAX_OUTPUTS
/*
* struct sit9531x_chip_info - chip variant identification
@@ -41,6 +51,39 @@ struct sit9531x_chip_info {
const u8 *clkout_map;
};
+/*
+ * enum sit9531x_signal_mode - input signal electrical mode
+ * @SIT9531X_MODE_SE: single-ended
+ * @SIT9531X_MODE_DE: differential
+ */
+enum sit9531x_signal_mode {
+ SIT9531X_MODE_SE = 0,
+ SIT9531X_MODE_DE,
+};
+
+/*
+ * struct sit9531x_ref - input reference state
+ * @freq: configured frequency in Hz
+ * @label: board label from DT or default
+ * @sig_mode: signal mode of the pair this lane belongs to
+ * (detected from CLKINx_INPUT_MODE at probe)
+ */
+struct sit9531x_ref {
+ u32 freq;
+ const char *label;
+ enum sit9531x_signal_mode sig_mode;
+};
+
+/*
+ * struct sit9531x_out - output state
+ * @freq: configured frequency in Hz
+ * @label: board label from DT or default
+ */
+struct sit9531x_out {
+ u32 freq;
+ const char *label;
+};
+
/*
* struct sit9531x_dev - SiT9531x device instance
* @dev: parent device
@@ -48,6 +91,8 @@ struct sit9531x_chip_info {
* @regmap: paged register map
* @info: detected chip variant info
* @multiop_lock: serializes multi-register sequences
+ * @ref: array of input reference states
+ * @out: array of output states
* @xtal_freq: crystal oscillator frequency in Hz
* @reset_gpio: optional reset line (DT "reset-gpios"), NULL if absent
*/
@@ -59,11 +104,38 @@ struct sit9531x_dev {
/* Serializes multi-step register sequences */
struct mutex multiop_lock;
+ /* Hardware state */
+ struct sit9531x_ref ref[SIT9531X_MAX_INPUTS + 1]; /* +1 for xtal */
+ struct sit9531x_out out[SIT9531X_MAX_OUTPUTS];
u32 xtal_freq;
struct gpio_desc *reset_gpio;
};
+/*
+ * Logical input pins are interleaved: even index = P lane, odd
+ * index = N lane of pair index/2 (IN0P, IN0N, IN1P, IN1N, ...).
+ * Index SIT9531X_MAX_INPUTS is the XO input.
+ */
+
+/*
+ * sit9531x_input_pair - get input pair number for a logical input index
+ * @index: logical input pin index
+ */
+static inline u8 sit9531x_input_pair(u8 index)
+{
+ return index >> 1;
+}
+
+/*
+ * sit9531x_input_is_n - check if a logical input index is an N lane
+ * @index: logical input pin index
+ */
+static inline bool sit9531x_input_is_n(u8 index)
+{
+ return index & 1;
+}
+
/*
* sit9531x_pll_page - get register page for PLL index
* @pll_idx: PLL index (0 = PLLA, 3 = PLLD)
diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
new file mode 100644
index 000000000000..1a09bd168163
--- /dev/null
+++ b/drivers/dpll/sit9531x/prop.c
@@ -0,0 +1,430 @@
+// 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 and re-prioritised.
+ */
+ 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 the runtime ref->freq / out->freq with the first DT-listed
+ * supported frequency so the netlink frequency_get callback reports
+ * a sane initial value before any pin_set occurs. DT lists the
+ * physically-wired reference frequency for each input pin and the
+ * default output frequency for each output pin.
+ */
+ if (num_freqs > 0) {
+ if (dir != DPLL_PIN_DIRECTION_INPUT ||
+ index != SIT9531X_MAX_INPUTS)
+ curr_freq = freqs[0];
+ }
+
+skip_fwnode_props:
+ /* Neither INTSYNC pin carries a frequency attribute */
+ if (dir == DPLL_PIN_DIRECTION_INPUT &&
+ index == SIT9531X_INTSYNC_PIN_ID)
+ return props;
+ if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+ index == SIT9531X_INTSYNC_OUT_PIN_ID)
+ return props;
+
+ /*
+ * Advertise only concrete values from firmware plus current runtime
+ * value. For outputs without a firmware list, publish one wide range as
+ * an explicit fallback because those pins do support frequency_set.
+ */
+ ranges = kcalloc(num_freqs + 2, sizeof(*ranges), GFP_KERNEL);
+ if (!ranges) {
+ rc = -ENOMEM;
+ goto err_alloc_ranges;
+ }
+
+ /*
+ * Publish the seeded rate only once the pin is certain to be built.
+ * The allocation above is the last thing that can fail, and a call
+ * that reports failure must not leave the device's cached rate
+ * changed behind it.
+ */
+ if (curr_freq) {
+ if (dir == DPLL_PIN_DIRECTION_INPUT)
+ sitdev->ref[index].freq = curr_freq;
+ else
+ sitdev->out[index].freq = curr_freq;
+ }
+
+ j = 0;
+
+ /* Current frequency first, when known. */
+ if (curr_freq) {
+ 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] 28+ messages in thread* Re: [PATCH v10 04/14] dpll: sit9531x: read DPLL types and pin properties from system firmware
2026-09-21 20:11 ` [PATCH v10 04/14] dpll: sit9531x: read DPLL types and pin properties from system firmware Ali Rouhi
@ 2026-09-26 2:34 ` Jakub Kicinski
0 siblings, 0 replies; 28+ messages in thread
From: Jakub Kicinski @ 2026-09-26 2:34 UTC (permalink / raw)
To: arouhi
Cc: Jakub Kicinski, jiri, vadim.fedorenko, arkadiusz.kubalewski,
ivecera, robh, krzk+dt, conor+dt, cjubran, pabeni,
Oleg.Zadorozhnyi, prabhakar.mahadev-lad.rj, dev, devicetree,
netdev, linux-kernel
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
dpll: sit9531x: read DPLL types and pin properties from system firmware
This commit adds prop.c, which reads the DPLL type for each channel and
the properties for each pin from the firmware node. The pin properties
are the package label, board label, type, capabilities and supported
frequencies, with defaults when the node does not describe them. It also
adds the input and output state structures to core.h, plus helpers that
map a logical input index to a pair and a lane.
> Two of the advertised properties are worth naming. Outputs get a
> phase-adjust window of 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.
[Severity: Low]
Is this paragraph describing code that is in this patch? At this commit,
nothing under drivers/dpll/sit9531x/ sets dpll_props.phase_range or
dpll_props.phase_gran. The output branch of sit9531x_pin_props_get()
sets only type, capabilities and curr_freq.
The phase window, and a comment giving the same reasons, arrive later in
"dpll: sit9531x: add support to adjust output phase". The v10 notes say
the window was moved to that patch, but this paragraph was left here.
Could it move to that patch's commit message?
A smaller mismatch of the same kind is the INTSYNC "re-prioritised"
comment in sit9531x_pin_props_get(), noted further down.
> diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
> index 76a2632f0ce4a..230b21b9e2381 100644
> --- a/drivers/dpll/sit9531x/core.h
> +++ b/drivers/dpll/sit9531x/core.h
[ ... ]
> @@ -41,6 +51,39 @@ struct sit9531x_chip_info {
> const u8 *clkout_map;
> };
>
[ ... ]
> +/*
> + * struct sit9531x_ref - input reference state
> + * @freq: configured frequency in Hz
> + * @label: board label from DT or default
> + * @sig_mode: signal mode of the pair this lane belongs to
> + * (detected from CLKINx_INPUT_MODE at probe)
> + */
> +struct sit9531x_ref {
> + u32 freq;
[Severity: Low]
Can this u32 truncate the u64 curr_freq that sit9531x_pin_props_get()
stores here and in sit9531x_out::freq? A rate of 2^32 Hz or more would
be cached truncated, while the full value is advertised in
freq_supported.
This can't be reached in any tree state. The next patch, "dpll:
sit9531x: register DPLL devices and pins", adds the first caller and
widens both fields to u64 in the same change. Could they simply be u64
from this patch?
> + const char *label;
> + enum sit9531x_signal_mode sig_mode;
> +};
> +
> +/*
> + * struct sit9531x_out - output state
> + * @freq: configured frequency in Hz
> + * @label: board label from DT or default
> + */
> +struct sit9531x_out {
> + u32 freq;
> + const char *label;
> +};
[Severity: Low]
Is anything meant to write sit9531x_ref::label and sit9531x_out::label?
Both are documented as "board label from DT or default", but nothing in
the driver assigns them, either here or at the end of the series.
The DT label goes into props->dpll_props.board_label in
sit9531x_pin_props_get(). The package label goes into
props->package_label. Could these fields be dropped, or their
documentation corrected?
sig_mode is also never set at this commit, so the differential "IN%u"
branch of sit9531x_input_pin_label() cannot be reached yet. That part is
fixed by sit9531x_input_mode_fetch() in "dpll: sit9531x: register DPLL
devices and pins", which runs before any pin reaches this code.
[ ... ]
> diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
> new file mode 100644
> index 0000000000000..1a09bd168163d
> --- /dev/null
> +++ b/drivers/dpll/sit9531x/prop.c
> @@ -0,0 +1,430 @@
[ ... ]
> +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];
> + }
[ ... ]
> + /* 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;
> + }
[Severity: Medium]
How is a DT author expected to number reg in these nodes? Two different
numberings are used here. Neither dpll-pin.yaml nor sitime,sit95316.yaml
defines either one.
For inputs, reg is compared directly with the interleaved lane index:
2k is INkP and 2k+1 is INkN. The binding only says there are "4
differential input pairs whose lanes can also be driven independently as
single-ended references, so 8 inputs are individually selectable".
A differential pair k is registered only as index 2k, because
sit9531x_input_pin_is_registrable() skips the N lane. Say a board
describes differential IN1 with reg = <1>. Would its label,
connection-type and supported-frequencies-hz be applied to IN0N when
pair 0 is single-ended? And would the node be silently ignored when
pair 0 is differential?
The input pin@1 in the binding example ("clkin1", synce) only works if
pair 0 is strapped single-ended.
For outputs, the logical index goes through clkout_map[] first. On
SiT95317 that means reg has to be one of 0, 3, 4, 5, 7, 8, 9 or 11.
In the same device node, sitime,output-pll-map is indexed by logical
output number. The binding describes it as "for each output 0..11" and
caps it at 8 items for SiT95317. sit9531x_parse_board_config() stores it
by logical index:
drivers/dpll/sit9531x/core.c:sit9531x_parse_board_config() {
...
for (i = 0; i < n; i++)
sitdev->out_pll_map[i] = map[i];
...
}
So on SiT95317, output-pll-map entry 1 refers to the pad labelled OUT3,
but output-pins reg = <1> refers to a slot that is not bonded out. A
node written like the output pin@1 in the binding example would match
nothing. That output would then silently get the 1 Hz to 1 GHz fallback
range instead of the board's list.
Could the binding document both numberings? Could the driver also warn
when a pin node's reg matches no registered pin?
[ ... ]
> + if (dir == DPLL_PIN_DIRECTION_INPUT &&
> + index == SIT9531X_INTSYNC_PIN_ID) {
> + /*
> + * INTSYNC destination pin: a PLL locks to the INTSYNC net as a
> + * reference, so it can be connected and re-prioritised.
> + */
> + props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
> + props->dpll_props.capabilities =
> + DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
[Severity: Low]
The comment says this pin can be re-prioritised, but the mask only has
DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE. The physical input branch below
uses the same mask. With this mask, wouldn't the DPLL core reject every
priority change on these pins?
Nothing is registered at this commit and there are no prio ops yet.
"dpll: sit9531x: add support to get and set priority on input pins" adds
DPLL_PIN_CAPABILITIES_PRIORITY_CAN_CHANGE to both the INTSYNC
destination pin and the physical inputs, so the series fixes this
later. Could the comment and the capability land in the same patch?
[ ... ]
> + /*
> + * Seed the runtime ref->freq / out->freq with the first DT-listed
> + * supported frequency so the netlink frequency_get callback reports
> + * a sane initial value before any pin_set occurs. DT lists the
> + * physically-wired reference frequency for each input pin and the
> + * default output frequency for each output pin.
> + */
> + if (num_freqs > 0) {
> + if (dir != DPLL_PIN_DIRECTION_INPUT ||
> + index != SIT9531X_MAX_INPUTS)
> + curr_freq = freqs[0];
> + }
[Severity: Low]
For output pins, this replaces curr_freq, taken from
sitdev->out[index].freq, with the first supported-frequencies-hz entry.
That value is then cached back into sitdev->out[index].freq further
down. Doesn't that contradict the v10 note "Took an output's current
rate from its divider instead of the first entry of the list of rates
the board supports"?
No output pin ever reaches this code while it behaves this way:
- sit9531x_pin_props_get() has no caller at this commit.
- The next patch registers only inputs and XO.
- "dpll: sit9531x: add support to get and set frequency on pins" limits
the seeding to inputs and reads outputs back through
sit9531x_output_freq_get(), before any output pin is registered.
For inputs, taking the first entry as the wired rate is intentional,
according to that later patch. The convention is not written down in
dpll-pin.yaml, though. Should the binding mention it?
[ ... ]
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH v10 05/14] dpll: sit9531x: register DPLL devices and pins
2026-09-21 20:11 [PATCH v10 00/14] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (3 preceding siblings ...)
2026-09-21 20:11 ` [PATCH v10 04/14] dpll: sit9531x: read DPLL types and pin properties from system firmware Ali Rouhi
@ 2026-09-21 20:11 ` Ali Rouhi
2026-09-26 2:34 ` Jakub Kicinski
2026-09-21 20:11 ` [PATCH v10 06/14] dpll: sit9531x: implement input pin state on a DPLL Ali Rouhi
` (8 subsequent siblings)
13 siblings, 1 reply; 28+ messages in thread
From: Ali Rouhi @ 2026-09-21 20:11 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi,
prabhakar.mahadev-lad.rj, dev, devicetree, netdev, linux-kernel,
Ali Rouhi
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Register each of the four on-chip PLLs as a DPLL device, and the 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.
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. Where a board wires INTRB, the
interrupt schedules that tick immediately rather than replacing it: the
poll stays as the fallback, since this device gives no way to be sure an
edge was not missed.
The clock_id that keys a device in the DPLL core is built from the chip's
variant and its position on the bus, with the adapter number carried in
eight bits. Two identical parts at the same address on adapters whose
numbers differ by a multiple of 256 would therefore collide. That needs a
machine with more than 256 I2C adapters and the same part twice at the
same address on two of them; the alternative is a wider identifier that no
other driver expects, so the constraint is stated rather than designed
around.
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
Notes:
Changes in v10:
Interrupt: give the line up after repeated failures to clear the
notification latch instead of re-entering the handler behind a
still-asserted INTRB.
Cache coherence: re-derive the input receiver refcounts from the
priority tables the poll reads, so a request that failed part way
cannot leave a receiver gated off while a PLL is still using it.
Dropped two per-input status flags, and the register read that filled
them, that nothing consumed.
Notification: treat a change in the lock-status error like a change in
the status itself.
Left the probe message naming the variant, which the series split had
dropped, and declared each helper in the patch that defines it.
Corrected the kernel-doc that had stopped listing members the structs
still have, and the changelog about what this patch registers and what
the poll caches.
Dropped the per-pin embedded-sync state, and the two constants
that only the withdrawn patch used.
drivers/dpll/sit9531x/Kconfig | 5 +
drivers/dpll/sit9531x/Makefile | 2 +-
drivers/dpll/sit9531x/core.c | 1629 ++++++++++++++++++++++++++++++--
drivers/dpll/sit9531x/core.h | 260 ++++-
drivers/dpll/sit9531x/dpll.c | 450 +++++++++
drivers/dpll/sit9531x/dpll.h | 66 ++
drivers/dpll/sit9531x/regs.h | 220 ++++-
7 files changed, 2530 insertions(+), 102 deletions(-)
create mode 100644 drivers/dpll/sit9531x/dpll.c
create mode 100644 drivers/dpll/sit9531x/dpll.h
diff --git a/drivers/dpll/sit9531x/Kconfig b/drivers/dpll/sit9531x/Kconfig
index c306d1dd1c58..47aea8674327 100644
--- a/drivers/dpll/sit9531x/Kconfig
+++ b/drivers/dpll/sit9531x/Kconfig
@@ -3,10 +3,15 @@
config SIT9531X_DPLL
tristate "SiTime SiT9531x DPLL driver"
depends on I2C && NET
+ select DPLL
select REGMAP_I2C
help
Driver for SiTime SiT9531x family clock generators
(SiT95317, SiT95316).
+ This driver registers each on-chip PLL as a DPLL device
+ and exposes input/output clocks as DPLL pins, providing
+ runtime configuration via Generic Netlink.
+
To compile this driver as a module, choose M here: the
module will be called sit9531x.
diff --git a/drivers/dpll/sit9531x/Makefile b/drivers/dpll/sit9531x/Makefile
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 cf1cda87fa52..6f19dab89bee 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) }
@@ -161,122 +180,1580 @@ int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
return regmap_update_bits(sitdev->regmap, reg, mask, val);
}
-static int sit9531x_read_variant_id(struct sit9531x_dev *sitdev, u8 *id)
+/*
+ * sit9531x_input_get_regs - get force mask and state register addresses
+ * @index: logical input index
+ * @force_reg: output force mask register address
+ * @state_reg: output state register address
+ *
+ * Selects the correct Page 0x02 register pair based on the pair's
+ * signal mode and the lane (P/N) the index refers to.
+ */
+static void sit9531x_input_get_regs(const struct sit9531x_dev *sitdev,
+ u8 index,
+ unsigned int *force_reg,
+ unsigned int *state_reg)
{
- return sit9531x_read_u8(sitdev, SIT9531X_REG_VARIANT_ID, id);
+ if (sitdev->ref[index].sig_mode == SIT9531X_MODE_DE) {
+ *force_reg = SIT9531X_REG_IN_DE_FORCE;
+ *state_reg = SIT9531X_REG_IN_DE_STATE;
+ } else if (sit9531x_input_is_n(index)) {
+ *force_reg = SIT9531X_REG_IN_SEN_FORCE;
+ *state_reg = SIT9531X_REG_IN_SEN_STATE;
+ } else {
+ *force_reg = SIT9531X_REG_IN_SEP_FORCE;
+ *state_reg = SIT9531X_REG_IN_SEP_STATE;
+ }
}
-static const struct sit9531x_chip_info *sit9531x_match_variant(u8 id)
-{
- unsigned int i;
+/*
+ * Output enable / disable (Hi-Z control)
+ *
+ * SiT9531x outputs can be configured as differential (DIFF) or
+ * single-ended (SE) depending on the factory blob. Each output slot
+ * has TWO Hi-Z force/state register pairs on Page 0x03 -- one for the
+ * DIFF path, one for the SE path.
+ *
+ * We write to BOTH pairs so the function mutes the output regardless
+ * of whether it's been configured DIFF or SE on this board.
+ *
+ * slot 0-7 :
+ * DIFF mask=0xF2 state=0xF3 SE mask=0xF8 state=0xF9
+ * slot 8-11:
+ * DIFF mask=0xF4 state=0xF5 SE mask=0xFA state=0xFB
+ *
+ * MASK bit = 1 -> driver takes control of that output's Hi-Z state
+ * STATE bit = 0 -> output is forced to Hi-Z (muted)
+ * STATE bit = 1 -> output is driven (active)
+ *
+ * The output "index" in the driver is logical; the physical slot comes
+ * from info->clkout_map[].
+ */
- for (i = 0; i < ARRAY_SIZE(sit9531x_chip_ids); i++) {
- if (sit9531x_chip_ids[i].id == id)
- return &sit9531x_chip_ids[i];
+struct sit9531x_hiz_regs {
+ unsigned int diff_mask;
+ unsigned int diff_state;
+ unsigned int se_mask;
+ unsigned int se_state;
+ u8 bit;
+};
+
+static void sit9531x_output_get_hiz_regs(u8 slot,
+ struct sit9531x_hiz_regs *r)
+{
+ if (slot <= 7) {
+ r->diff_mask = SIT9531X_REG_HIZ_DIFF_07_MASK;
+ r->diff_state = SIT9531X_REG_HIZ_DIFF_07_STATE;
+ r->se_mask = SIT9531X_REG_HIZ_SE_07_MASK;
+ r->se_state = SIT9531X_REG_HIZ_SE_07_STATE;
+ r->bit = slot;
+ } else {
+ r->diff_mask = SIT9531X_REG_HIZ_DIFF_811_MASK;
+ r->diff_state = SIT9531X_REG_HIZ_DIFF_811_STATE;
+ r->se_mask = SIT9531X_REG_HIZ_SE_811_MASK;
+ r->se_state = SIT9531X_REG_HIZ_SE_811_STATE;
+ r->bit = slot - 8;
}
+}
- return NULL;
+/*
+ * sit9531x_output_mode_fetch - read how an output is wired
+ *
+ * The Hi-Z force is a separate register pair for the differential and the
+ * single-ended path, and only the pair belonging to the way the output is
+ * actually wired says anything about whether it is quiet. The other pair
+ * holds whatever the loaded configuration left in it.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_output_mode_fetch(struct sit9531x_dev *sitdev, u8 out_idx)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ u8 slot, page, reg, val;
+ int rc;
+
+ slot = info->clkout_map[out_idx];
+ page = (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX) ?
+ SIT9531X_PAGE_OUTSYS1 : SIT9531X_PAGE_OUTSYS0;
+ reg = SIT9531X_OUT_MISC0_BASE +
+ SIT9531X_OUT_MISC0_STRIDE * (slot % 6);
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG(page, reg), &val);
+ if (rc)
+ return rc;
+
+ sitdev->out[out_idx].cmos = !!(val & (SIT9531X_OUT_CMOS_ENP |
+ SIT9531X_OUT_CMOS_ENN));
+
+ return 0;
}
-int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
+/*
+ * Report whether a slot is currently forced into Hi-Z, i.e. the driver
+ * (or the blob) took control of the Hi-Z state (MASK bit set) and drives
+ * it low (STATE bit clear). Either register pair muting the slot counts,
+ * mirroring what sit9531x_output_disable() programs.
+ */
+static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev,
+ u8 out_idx, bool *muted)
{
- struct clk *xtal_clk;
- u8 variant_id;
+ struct sit9531x_hiz_regs r;
+ unsigned int mask_reg, state_reg;
+ u8 mask, state;
int rc;
+ sit9531x_output_get_hiz_regs(sitdev->info->clkout_map[out_idx], &r);
+
/*
- * Fvco = Fref * (DIVN + frac/2^32) with Fref derived from the XO
- * feeding XIN/XO_CLK, so the rate is needed before anything can be
- * computed from a divider. The rate normally comes from a "clocks"
- * phandle (clock-names = "xtal"). On platforms where the firmware
- * does not expose the XO through the clock framework, fall back to
- * a "clock-frequency" device property.
+ * Read the pair that belongs to the way this output is wired.
+ * Testing both and taking either as proof of a mute answers
+ * from a register nothing drives, and disagrees with itself
+ * when a mute lands on one pair and fails on the other.
*/
- xtal_clk = devm_clk_get_optional_enabled(sitdev->dev, "xtal");
- if (IS_ERR(xtal_clk))
- return dev_err_probe(sitdev->dev, PTR_ERR(xtal_clk),
- "Failed to get xtal clock\n");
- sitdev->xtal_freq = clk_get_rate(xtal_clk);
- if (!sitdev->xtal_freq) {
- u32 freq;
+ if (sitdev->out[out_idx].cmos) {
+ mask_reg = r.se_mask;
+ state_reg = r.se_state;
+ } else {
+ mask_reg = r.diff_mask;
+ state_reg = r.diff_state;
+ }
- if (!device_property_read_u32(sitdev->dev, "clock-frequency",
- &freq))
- sitdev->xtal_freq = freq;
+ rc = sit9531x_read_u8(sitdev, mask_reg, &mask);
+ if (rc)
+ return rc;
+ rc = sit9531x_read_u8(sitdev, state_reg, &state);
+ if (rc)
+ return rc;
+
+ *muted = (mask & BIT(r.bit)) && !(state & BIT(r.bit));
+
+ return 0;
+}
+
+/*
+ * Input priority selection
+ *
+ * The SiT9531x has an 11-slot priority table per PLL on Page 1. Each
+ * register holds two slots nibble-packed: the earlier (even, 2n) slot
+ * in [7:4] and the later (odd, 2n+1) slot in [3:0].
+ *
+ * The procedure:
+ * 1. Force PLL into holdover (PLL page reg 0x6F bit 4)
+ * 2. Write priority slots on Page 1
+ * 3. Small change update (Page 0 reg 0x0F bit 1)
+ * 4. Release holdover
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+
+/* Page-1 register holding priority slot @slot of @pll_idx. */
+static u16 sit9531x_prio_reg(u8 pll_idx, u8 slot)
+{
+ return SIT9531X_REG(SIT9531X_PAGE_PRIOSYS,
+ SIT9531X_PRIO_BASE_REG +
+ SIT9531X_PRIO_REGS_PER_PLL * pll_idx +
+ slot / SIT9531X_PRIO_SLOTS_PER_REG);
+}
+
+/*
+ * Extract priority slot @slot from its register value. The register
+ * holding slots 2n and 2n+1 keeps the earlier slot in the high nibble
+ * (CLK_SPARE<2n>SEL) and the later one in the low nibble.
+ */
+static u8 sit9531x_prio_slot_get(u8 val, u8 slot)
+{
+ if (slot & 1)
+ return val & SIT9531X_PRIO_NIBBLE_MASK;
+
+ return val >> SIT9531X_PRIO_HI_SHIFT;
+}
+
+/*
+ * Rebuild a PLL's membership mask from the source codes of its priority
+ * table. The mask is what the pin state getters test, so it is refreshed
+ * from exactly the values the table holds -- here after a write, and once
+ * per poll from the read-back in sit9531x_chan_state_fetch().
+ */
+static void sit9531x_prio_mask_build(struct sit9531x_dev *sitdev, u8 pll_idx,
+ const u8 *srcs)
+{
+ u16 mask = 0;
+ u8 slot;
+
+ for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+ 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 LOS and OOF status for the given input lane from the Page 0x06
+ * clock monitor registers. P and N lanes have separate register banks;
+ * each register carries two input pairs nibble-packed (even pair in
+ * bits [3:0], odd pair in [7:4]).
+ */
+static int sit9531x_ref_state_fetch(struct sit9531x_dev *sitdev, u8 index)
+{
+ unsigned int 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)
+/*
+ * sit9531x_chan_state_fetch - read PLL channel status from hardware
+ *
+ * Reads lock status and mode from the PLL status register.
+ */
+/* Read the PLL active-state bit (PLL page reg 0x02 bit 0). */
+static int sit9531x_pll_is_active(struct sit9531x_dev *sitdev, u8 pll_idx,
+ bool *active)
{
- 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);
+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;
}
-static const struct of_device_id sit9531x_of_match[] = {
- { .compatible = "sitime,sit95316" },
- { .compatible = "sitime,sit95317" },
- { }
-};
-MODULE_DEVICE_TABLE(of, sit9531x_of_match);
+/*
+ * sit9531x_out_state_fetch - read output status from hardware
+ *
+ * Reads the output PLL association from the PLL page output map
+ * registers into out->routed / out->pll_idx, and the current drive
+ * state from the Hi-Z force bits into out->enabled. The two are
+ * separate: routing decides whether the output gets a DPLL pin at all,
+ * while a muted but routed output keeps its pin and reports
+ * DPLL_PIN_STATE_DISCONNECTED until it is un-muted.
+ */
+static int sit9531x_out_state_fetch(struct sit9531x_dev *sitdev, u8 index)
+{
+ struct sit9531x_out *out = &sitdev->out[index];
+ u8 map_lo, map_hi, slot;
+ int pll_idx;
+ bool muted;
+ int rc;
+
+ slot = sitdev->info->clkout_map[index];
+
+ rc = sit9531x_output_mode_fetch(sitdev, index);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_output_forced_hiz(sitdev, index, &muted);
+ if (rc)
+ return rc;
+
+ /*
+ * The OUT_MAP_LO/HI bitmaps are indexed by the physical slot the
+ * output occupies on the chip, not by the driver's logical output
+ * index (translated above via the chip-info clkout_map[]: identity
+ * on SiT95316, non-contiguous on SiT95317).
+ *
+ * Determine which PLL drives this output by checking each PLL's
+ * output map registers (0x27 = slots 8-11, 0x28 = slots 0-7).
+ */
+ for (pll_idx = 0; pll_idx < SIT9531X_NUM_PLLS; pll_idx++) {
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_OUT_MAP_LO, &map_lo);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_OUT_MAP_HI, &map_hi);
+ if (rc)
+ return rc;
+
+ if (slot < 8) {
+ if (map_lo & BIT(slot)) {
+ out->pll_idx = pll_idx;
+ out->routed = true;
+ out->enabled = !muted;
+ return 0;
+ }
+ } else {
+ if (map_hi & BIT(slot - 8)) {
+ out->pll_idx = pll_idx;
+ out->routed = true;
+ out->enabled = !muted;
+ return 0;
+ }
+ }
+ }
+
+ /* Output not mapped to any PLL */
+ out->pll_idx = 0;
+ out->routed = false;
+ out->enabled = false;
+
+ return 0;
+}
+
+/*
+ * sit9531x_ref_pll_mask_fetch - seed the input-to-PLL usage masks
+ *
+ * ref->pll_mask is the refcount the disconnect path uses to decide when
+ * an input receiver may be powered down: the physical input is only
+ * disabled once the last DPLL has released it. It therefore has to
+ * start out matching the hardware. Without this pass every mask starts
+ * at zero, and disconnecting an input from one DPLL drops the mask to
+ * zero and disables a receiver the other DPLLs are still locked to.
+ *
+ * An input is counted for a PLL when it appears in that PLL's Page-1
+ * priority table, which is exactly the condition the connect and
+ * disconnect callbacks maintain. Sources that are not physical inputs
+ * (OCXO, INTSYNC) and reserved codes are skipped.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_ref_pll_mask_fetch(struct sit9531x_dev *sitdev)
+{
+ u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+ u8 pll_idx, slot, index;
+ int rc;
+
+ for (pll_idx = 0; pll_idx < SIT9531X_NUM_PLLS; pll_idx++) {
+ rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+ if (rc)
+ return rc;
+
+ for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+ index = sit9531x_hw_src_input(srcs[slot]);
+ if (index >= sitdev->info->num_inputs)
+ continue;
+
+ /*
+ * On a differentially configured pair only the P lane
+ * has a DPLL pin, so that is the lane the connect and
+ * disconnect callbacks account for. Fold an N-lane
+ * table entry onto its P lane, otherwise the count
+ * would land on a lane nothing ever releases. The
+ * signaling mode is already known here:
+ * sit9531x_input_mode_fetch() runs first.
+ */
+ if (sit9531x_input_is_n(index) &&
+ sitdev->ref[index].sig_mode == SIT9531X_MODE_DE)
+ index--;
+
+ sitdev->ref[index].pll_mask |= BIT(pll_idx);
+ }
+ }
+
+ return 0;
+}
+
+/*
+ * sit9531x_dev_state_fetch - read all hardware state at startup
+ *
+ * Called once during probe to populate the initial state cache.
+ */
+static int sit9531x_dev_state_fetch(struct sit9531x_dev *sitdev)
+{
+ int rc;
+ u8 i;
+
+ /* Detect SE/DE configuration before any per-lane access */
+ for (i = 0; i < sitdev->info->num_inputs / 2; i++) {
+ rc = sit9531x_input_mode_fetch(sitdev, i);
+ if (rc) {
+ dev_err(sitdev->dev,
+ "Failed to fetch CLKIN%u mode: %d\n", i, rc);
+ return rc;
+ }
+ }
+
+ for (i = 0; i < sitdev->info->num_inputs; i++) {
+ rc = sit9531x_ref_state_fetch(sitdev, i);
+ if (rc) {
+ dev_err(sitdev->dev,
+ "Failed to fetch input %u state: %d\n", i, rc);
+ return rc;
+ }
+ }
+
+ /*
+ * The priority-table read walks the Page-1 registers, so it runs
+ * with multiop_lock held like every other multi-register sequence.
+ * Nothing can race with it here -- the DPLLs are not registered and
+ * the monitor is not running yet -- but the page handling stays
+ * serialized the same way as at runtime.
+ */
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_ref_pll_mask_fetch(sitdev);
+ mutex_unlock(&sitdev->multiop_lock);
+ if (rc) {
+ dev_err(sitdev->dev,
+ "Failed to fetch input priority tables: %d\n", rc);
+ return rc;
+ }
+
+ for (i = 0; i < sitdev->info->num_outputs; i++) {
+ rc = sit9531x_out_state_fetch(sitdev, i);
+ if (rc) {
+ dev_err(sitdev->dev,
+ "Failed to fetch output %u state: %d\n", i, rc);
+ return rc;
+ }
+ }
+
+ for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+ rc = sit9531x_chan_state_fetch(sitdev, i);
+ if (rc) {
+ dev_err(sitdev->dev,
+ "Failed to fetch PLL%c state: %d\n",
+ 'A' + i, rc);
+ return rc;
+ }
+ }
+
+ return 0;
+}
+
+static void sit9531x_dev_ref_states_update(struct sit9531x_dev *sitdev)
+{
+ int i, rc;
+
+ for (i = 0; i < sitdev->info->num_inputs; i++) {
+ rc = sit9531x_ref_state_fetch(sitdev, i);
+ if (rc)
+ dev_warn(sitdev->dev,
+ "Failed to get REF%u status: %d\n", i, rc);
+ }
+}
+
+static void sit9531x_dev_chan_states_update(struct sit9531x_dev *sitdev)
+{
+ int i, rc;
+
+ for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+ rc = sit9531x_chan_state_fetch(sitdev, i);
+ if (rc)
+ dev_warn(sitdev->dev,
+ "Failed to get PLL%c state: %d\n",
+ 'A' + i, rc);
+ }
+}
+
+/*
+ * sit9531x_dev_periodic_work - periodic hardware state polling
+ * @work: kthread_work pointer
+ *
+ * Polls hardware state at SIT9531X_STATUS_POLL_MS intervals.
+ * Updates reference and channel states, then delegates change
+ * detection to sit9531x_dpll_changes_check() for each registered DPLL.
+ */
+static void sit9531x_dev_periodic_work(struct kthread_work *work)
+{
+ struct sit9531x_dev *sitdev = container_of(work, struct sit9531x_dev,
+ work.work);
+ struct sit9531x_dpll *sitdpll;
+ int rc;
+
+ /*
+ * Update the cached ref[]/chan[] arrays under multiop_lock so the
+ * fetches are serialized against the DPLL callbacks that read
+ * these fields and against the chip's page selector.
+ *
+ * The lock is then dropped before sit9531x_dpll_changes_check(),
+ * which calls dpll_pin_change_ntf() / dpll_device_change_ntf().
+ * Those notification helpers take DPLL-subsystem locks that are
+ * already held when our callbacks are invoked from netlink
+ * context, and nesting multiop_lock around them would invert the
+ * lock order. changes_check() reads the cache published above,
+ * which is already consistent.
+ */
+ mutex_lock(&sitdev->multiop_lock);
+ sit9531x_dev_ref_states_update(sitdev);
+ sit9531x_dev_chan_states_update(sitdev);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ list_for_each_entry(sitdpll, &sitdev->dplls, list)
+ sit9531x_dpll_changes_check(sitdpll);
+
+ /*
+ * Acknowledge the chip's notification latches after the tick has
+ * read and acted on them. Without this, the W1C bits remain set
+ * and -- on boards that wire INTRB -- the line stays asserted,
+ * re-firing the threaded handler back to back. The helper writes
+ * W1C bits across page 0 and page 6 and must run under
+ * multiop_lock to serialize the page selector against userspace
+ * dpll ops. Failure is non-fatal: status was already consumed
+ * for this tick and the next tick re-processes whatever stayed
+ * latched.
+ */
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_clear_notifications(sitdev);
+ mutex_unlock(&sitdev->multiop_lock);
+ if (rc)
+ dev_warn_ratelimited(sitdev->dev,
+ "Failed to clear notifications: %d\n",
+ rc);
+
+ /* Run twice a second */
+ kthread_queue_delayed_work(sitdev->kworker, &sitdev->work,
+ msecs_to_jiffies(SIT9531X_STATUS_POLL_MS));
+}
+
+/*
+ * sit9531x_irq_thread_fn - threaded IRQ handler for the chip's INTRB line
+ *
+ * Triggered when the chip asserts INTRB (and only when DT wires up the
+ * client interrupt; absent property == handler never installed). The
+ * action mirrors a periodic-work tick: queue an immediate run so status
+ * registers are read and DPLL changes_check fires without waiting for
+ * the next poll deadline. Polling continues to run as a fallback.
+ */
+static irqreturn_t sit9531x_irq_thread_fn(int irq, void *data)
+{
+ struct sit9531x_dev *sitdev = data;
+ int rc;
+
+ /*
+ * Acknowledge the chip's notification latches from the threaded
+ * handler itself. With IRQF_ONESHOT the line is unmasked on
+ * return, so deferring the W1C clear to the async kworker would
+ * let a still-asserted INTRB re-fire immediately (interrupt storm).
+ * Clear here, then kick the poll worker to read state and run
+ * changes_check.
+ */
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_clear_notifications(sitdev);
+ mutex_unlock(&sitdev->multiop_lock);
+ if (rc) {
+ dev_warn_ratelimited(sitdev->dev,
+ "IRQ: failed to clear notifications: %d\n",
+ rc);
+ /*
+ * The latch was not acknowledged, so with IRQF_ONESHOT the
+ * still-asserted line 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;
+}
+
+/*
+ * sit9531x_dev_start - start normal operation
+ *
+ * Fetches initial hardware state, registers all DPLL devices and
+ * their pins, and starts the periodic monitoring thread.
+ */
+/*
+ * Report what the device loaded from its EEPROM, and warn if it does not
+ * look like a healthy load.
+ *
+ * A profile that failed to load leaves the part running something other
+ * than what the board was designed around -- dividers, output routing
+ * and priority tables all differ -- while every register still reads
+ * back a plausible value. Naming the profile and saying whether the
+ * load was clean turns that into something visible at startup instead of
+ * something inferred from measurements later.
+ *
+ * This only reports. Boards in this family may have their
+ * configuration pushed over I2C rather than held in an EEPROM, and there
+ * the CRC pair means nothing, so a mismatch is not grounds for refusing
+ * to drive the device.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static void sit9531x_eeprom_state_report(struct sit9531x_dev *sitdev)
+{
+ u32 rec_crc = 0, cal_crc = 0, prof_id = 0;
+ u8 notif, v;
+ int rc, i;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ /* Profile id: three bytes, least significant first. */
+ for (i = 2; i >= 0; i--) {
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_PROFILE_ID + i, &v);
+ if (rc)
+ return;
+ prof_id = prof_id << 8 | v;
+ }
+
+ dev_info(sitdev->dev, "profile id %u\n", prof_id);
+
+ /* Both CRCs: four bytes, most significant first. */
+ for (i = 0; i < 4; i++) {
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_REC_CRC + i, &v);
+ if (rc)
+ return;
+ rec_crc = rec_crc << 8 | v;
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_CAL_CRC + i, &v);
+ if (rc)
+ return;
+ cal_crc = cal_crc << 8 | v;
+ }
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_EEPROM_NOTIF, ¬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);
+ }
+}
+
+int sit9531x_dev_start(struct sit9531x_dev *sitdev)
+{
+ struct sit9531x_dpll *sitdpll;
+ int rc;
+
+ /* Fetch device state */
+ rc = sit9531x_dev_state_fetch(sitdev);
+ if (rc)
+ return rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+ sit9531x_eeprom_state_report(sitdev);
+ sit9531x_pll_states_report(sitdev);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ list_for_each_entry(sitdpll, &sitdev->dplls, list) {
+ rc = sit9531x_dpll_register(sitdpll);
+ if (rc) {
+ dev_err_probe(sitdev->dev, rc,
+ "Failed to register DPLL%u\n",
+ sitdpll->id);
+ goto err_unregister;
+ }
+ }
+
+ kthread_queue_delayed_work(sitdev->kworker, &sitdev->work, 0);
+
+ return 0;
+
+err_unregister:
+ /*
+ * Unregister what did register. The caller frees the list on this
+ * path, so leaving a DPLL registered would hand the subsystem a
+ * pointer to freed memory.
+ */
+ list_for_each_entry_continue_reverse(sitdpll, &sitdev->dplls, list)
+ sit9531x_dpll_unregister(sitdpll);
+
+ return rc;
+}
+
+/*
+ * sit9531x_dev_stop - stop normal operation
+ *
+ * Cancels the monitoring thread and unregisters all DPLL devices
+ * and their pins.
+ */
+void sit9531x_dev_stop(struct sit9531x_dev *sitdev)
+{
+ struct sit9531x_dpll *sitdpll;
+
+ kthread_cancel_delayed_work_sync(&sitdev->work);
+
+ list_for_each_entry(sitdpll, &sitdev->dplls, list) {
+ if (sitdpll->dpll_dev)
+ sit9531x_dpll_unregister(sitdpll);
+ }
+}
+
+static struct sit9531x_dpll_pin *
+sit9531x_dpll_pin_alloc(struct sit9531x_dpll *sitdpll,
+ enum dpll_pin_direction dir, u8 id)
+{
+ struct sit9531x_dpll_pin *pin;
+
+ pin = kzalloc_obj(*pin, GFP_KERNEL);
+ if (!pin)
+ return ERR_PTR(-ENOMEM);
+
+ pin->dpll = sitdpll;
+ pin->dir = dir;
+ pin->id = id;
+
+ return pin;
+}
+
+/*
+ * sit9531x_dpll_pin_register - register a DPLL pin with the subsystem
+ * @index: absolute pin index for clock_id namespace
+ *
+ * Gets pin properties from firmware, creates or gets a dpll_pin,
+ * and registers it with the parent DPLL device.
+ */
+static int sit9531x_dpll_pin_register(struct sit9531x_dpll_pin *pin,
+ u32 index)
+{
+ struct sit9531x_dpll *sitdpll = pin->dpll;
+ struct sit9531x_pin_props *props;
+ const struct dpll_pin_ops *ops;
+ int rc;
+
+ props = sit9531x_pin_props_get(sitdpll->dev, pin->dir, pin->id);
+ if (IS_ERR(props))
+ return PTR_ERR(props);
+
+ strscpy(pin->label, props->package_label, sizeof(pin->label));
+ pin->fwnode = fwnode_handle_get(props->fwnode);
+
+ pin->dpll_pin = dpll_pin_get(sitdpll->dev->clock_id, index,
+ THIS_MODULE, &props->dpll_props,
+ &pin->tracker);
+ if (IS_ERR(pin->dpll_pin)) {
+ rc = PTR_ERR(pin->dpll_pin);
+ goto err_pin_get;
+ }
+ dpll_pin_fwnode_set(pin->dpll_pin, props->fwnode);
+
+ ops = sit9531x_dpll_pin_ops_get(pin);
+
+ rc = dpll_pin_register(sitdpll->dpll_dev, pin->dpll_pin, ops, pin);
+ if (rc)
+ goto err_register;
+
+ sit9531x_pin_props_put(props);
+
+ return 0;
+
+err_register:
+ dpll_pin_put(pin->dpll_pin, &pin->tracker);
+err_pin_get:
+ /* dpll_pin_get() left an ERR_PTR here. */
+ pin->dpll_pin = NULL;
+ fwnode_handle_put(pin->fwnode);
+ pin->fwnode = NULL;
+ sit9531x_pin_props_put(props);
+
+ return rc;
+}
+
+static void sit9531x_dpll_pin_unregister(struct sit9531x_dpll_pin *pin)
+{
+ struct sit9531x_dpll *sitdpll = pin->dpll;
+ const struct dpll_pin_ops *ops;
+
+ ops = sit9531x_dpll_pin_ops_get(pin);
+
+ dpll_pin_unregister(sitdpll->dpll_dev, pin->dpll_pin, ops, pin);
+ dpll_pin_put(pin->dpll_pin, &pin->tracker);
+ pin->dpll_pin = NULL;
+
+ fwnode_handle_put(pin->fwnode);
+ pin->fwnode = NULL;
+}
+
+static void sit9531x_dpll_pins_unregister(struct sit9531x_dpll *sitdpll)
+{
+ struct sit9531x_dpll_pin *pin, *next;
+
+ list_for_each_entry_safe(pin, next, &sitdpll->pins, list) {
+ sit9531x_dpll_pin_unregister(pin);
+ list_del(&pin->list);
+ kfree(pin);
+ }
+}
+
+/*
+ * sit9531x_dpll_pin_is_registrable - check if a pin should be registered
+ * @dir: pin direction
+ * @index: pin hardware index
+ *
+ * Only the XO pin has a complete pin-op table in this patch, so only
+ * the XO pin is registrable here. Other pin classes are registered
+ * once their state callbacks land in the following patches.
+ *
+ * Return: true if pin should be registered, false otherwise
+ */
+static bool sit9531x_dpll_pin_is_registrable(struct sit9531x_dpll *sitdpll,
+ enum dpll_pin_direction dir,
+ u8 index)
+{
+ /*
+ * Only the XO pin has a complete pin-op table in this patch.
+ * Other pin classes are registered once their state callbacks
+ * land in the following patches.
+ */
+ if (dir != DPLL_PIN_DIRECTION_INPUT)
+ return false;
+
+ return index == SIT9531X_MAX_INPUTS;
+}
+
+/*
+ * sit9531x_dpll_pins_register - register all registrable pins
+ *
+ * Enumerates all possible input and output pins, checks registrability,
+ * and registers each one. Input pins come first, then output pins,
+ * with input pins first, then output pins.
+ */
+static int sit9531x_dpll_pins_register(struct sit9531x_dpll *sitdpll)
+{
+ struct sit9531x_dpll_pin *pin;
+ enum dpll_pin_direction dir;
+ u8 id, index;
+ int rc;
+
+ for (index = 0; index < SIT9531X_NUM_PINS_TOTAL; index++) {
+ if (index < SIT9531X_NUM_INPUT_PINS) {
+ id = index;
+ dir = DPLL_PIN_DIRECTION_INPUT;
+ } else {
+ id = index - SIT9531X_NUM_INPUT_PINS;
+ dir = DPLL_PIN_DIRECTION_OUTPUT;
+ }
+
+ if (!sit9531x_dpll_pin_is_registrable(sitdpll, dir, id))
+ continue;
+
+ pin = sit9531x_dpll_pin_alloc(sitdpll, dir, id);
+ if (IS_ERR(pin)) {
+ rc = PTR_ERR(pin);
+ goto error;
+ }
+
+ rc = sit9531x_dpll_pin_register(pin, index);
+ if (rc) {
+ kfree(pin);
+ goto error;
+ }
+
+ list_add(&pin->list, &sitdpll->pins);
+ }
+
+ return 0;
+
+error:
+ sit9531x_dpll_pins_unregister(sitdpll);
+ return rc;
+}
+
+static int sit9531x_dpll_device_register(struct sit9531x_dpll *sitdpll)
+{
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ int rc;
+
+ sitdpll->ops = sit9531x_dpll_device_ops;
+
+ sitdpll->dpll_dev = dpll_device_get(sitdev->clock_id, sitdpll->id,
+ THIS_MODULE, &sitdpll->tracker);
+ if (IS_ERR(sitdpll->dpll_dev)) {
+ rc = PTR_ERR(sitdpll->dpll_dev);
+ sitdpll->dpll_dev = NULL;
+ return rc;
+ }
+
+ rc = dpll_device_register(sitdpll->dpll_dev,
+ sit9531x_prop_dpll_type_get(sitdev,
+ sitdpll->id),
+ &sitdpll->ops, sitdpll);
+ if (rc) {
+ dpll_device_put(sitdpll->dpll_dev, &sitdpll->tracker);
+ sitdpll->dpll_dev = NULL;
+ }
+
+ return rc;
+}
+
+static void sit9531x_dpll_device_unregister(struct sit9531x_dpll *sitdpll)
+{
+ dpll_device_unregister(sitdpll->dpll_dev, &sitdpll->ops, sitdpll);
+ dpll_device_put(sitdpll->dpll_dev, &sitdpll->tracker);
+ sitdpll->dpll_dev = NULL;
+}
+
+/*
+ * sit9531x_dpll_alloc - allocate a DPLL device structure
+ * @sitdev: parent device
+ * @ch: PLL channel number (0-3)
+ *
+ * Return: pointer to allocated DPLL on success, error pointer on error
+ */
+struct sit9531x_dpll *sit9531x_dpll_alloc(struct sit9531x_dev *sitdev, u8 ch)
+{
+ struct sit9531x_dpll *sitdpll;
+
+ sitdpll = kzalloc_obj(*sitdpll, GFP_KERNEL);
+ if (!sitdpll)
+ return ERR_PTR(-ENOMEM);
+
+ sitdpll->dev = sitdev;
+ sitdpll->id = ch;
+ sitdpll->lock_status = DPLL_LOCK_STATUS_UNLOCKED;
+ INIT_LIST_HEAD(&sitdpll->pins);
+
+ return sitdpll;
+}
+
+/*
+ * sit9531x_dpll_free - deallocate a DPLL device structure
+ * @sitdpll: DPLL to free
+ */
+void sit9531x_dpll_free(struct sit9531x_dpll *sitdpll)
+{
+ kfree(sitdpll);
+}
+
+/*
+ * sit9531x_dpll_register - register DPLL device and all its pins
+ *
+ * Registers the DPLL device with the subsystem and then registers
+ * all input and output pins that are connected to this PLL.
+ */
+int sit9531x_dpll_register(struct sit9531x_dpll *sitdpll)
+{
+ int rc;
+
+ rc = sit9531x_dpll_device_register(sitdpll);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_dpll_pins_register(sitdpll);
+ if (rc) {
+ sit9531x_dpll_device_unregister(sitdpll);
+ return rc;
+ }
+
+ return 0;
+}
+
+/* sit9531x_dpll_unregister - unregister DPLL device and its pins */
+void sit9531x_dpll_unregister(struct sit9531x_dpll *sitdpll)
+{
+ sit9531x_dpll_pins_unregister(sitdpll);
+ sit9531x_dpll_device_unregister(sitdpll);
+}
+
+static void sit9531x_dpll_list_free(struct sit9531x_dev *sitdev)
+{
+ struct sit9531x_dpll *sitdpll, *next;
+
+ list_for_each_entry_safe(sitdpll, next, &sitdev->dplls, list) {
+ list_del(&sitdpll->list);
+ sit9531x_dpll_free(sitdpll);
+ }
+}
+
+/* Runs only once the device is fully started, see the caller. */
+static void sit9531x_dev_dpll_fini(void *ptr)
+{
+ struct sit9531x_dev *sitdev = ptr;
+
+ sit9531x_dev_stop(sitdev);
+ kthread_destroy_worker(sitdev->kworker);
+ sit9531x_dpll_list_free(sitdev);
+}
+
+static int sit9531x_devm_dpll_init(struct sit9531x_dev *sitdev)
+{
+ struct kthread_worker *kworker;
+ struct sit9531x_dpll *sitdpll;
+ unsigned int i;
+ int rc;
+
+ INIT_LIST_HEAD(&sitdev->dplls);
+ kthread_init_delayed_work(&sitdev->work, sit9531x_dev_periodic_work);
+
+ for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+ sitdpll = sit9531x_dpll_alloc(sitdev, i);
+ if (IS_ERR(sitdpll)) {
+ rc = dev_err_probe(sitdev->dev, PTR_ERR(sitdpll),
+ "Failed to alloc DPLL%u\n", i);
+ goto err_free_dplls;
+ }
+
+ list_add_tail(&sitdpll->list, &sitdev->dplls);
+ }
+
+ kworker = kthread_run_worker(0, "sit9531x-%s", dev_name(sitdev->dev));
+ if (IS_ERR(kworker)) {
+ rc = PTR_ERR(kworker);
+ goto err_free_dplls;
+ }
+ sitdev->kworker = kworker;
+
+ rc = sit9531x_dev_start(sitdev);
+ if (rc) {
+ rc = dev_err_probe(sitdev->dev, rc, "Failed to start device\n");
+ goto err_destroy_worker;
+ }
+
+ /*
+ * Only now is every field the cleanup touches valid, so this is the
+ * first point at which the action may be registered. On failure it
+ * runs the action itself, which is correct here and only here.
+ */
+ return devm_add_action_or_reset(sitdev->dev, sit9531x_dev_dpll_fini,
+ sitdev);
+
+err_destroy_worker:
+ kthread_destroy_worker(sitdev->kworker);
+err_free_dplls:
+ sit9531x_dpll_list_free(sitdev);
+
+ return rc;
+}
+
+/*
+ * sit9531x_read_variant_id - read chip variant ID byte from hardware
+ * @id: output variant ID byte
+ *
+ * Reads the single-byte variant identification register from Page 0
+ * reg 0x02 (95317 = 0x17, 95316 = 0x31). Reg 0x03 holds a separate
+ * revision byte and is intentionally not consumed here.
+ */
+static int sit9531x_read_variant_id(struct sit9531x_dev *sitdev, u8 *id)
+{
+ return sit9531x_read_u8(sitdev, SIT9531X_REG_VARIANT_ID, id);
+}
+
+static const struct sit9531x_chip_info *sit9531x_match_variant(u8 id)
+{
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(sit9531x_chip_ids); i++) {
+ if (sit9531x_chip_ids[i].id == id)
+ return &sit9531x_chip_ids[i];
+ }
+
+ return NULL;
+}
+
+/*
+ * sit9531x_derive_clock_id - build EUI-64 clock identifier
+ *
+ * Generates a deterministic 64-bit identifier from the SiTime OUI,
+ * the chip ID, the I2C bus number and the I2C address. The bus number
+ * disambiguates two same-variant parts at the same address on
+ * different adapters (or behind a mux), which the DPLL core would
+ * otherwise fold onto one set of objects; DT bus aliases keep the
+ * numbering, and with it the clock_id, stable across reboots.
+ *
+ * Return: 64-bit clock identifier
+ */
+static u64 sit9531x_derive_clock_id(struct sit9531x_dev *sitdev)
+{
+ u64 clkid;
+
+ clkid = SIT9531X_OUI << 24;
+ clkid |= (u64)(i2c_adapter_id(sitdev->client->adapter) & 0xff) << 16;
+ clkid |= (u64)sitdev->info->id << 8;
+ clkid |= (u64)sitdev->client->addr;
+
+ return clkid;
+}
+
+int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
+{
+ struct clk *xtal_clk;
+ u8 variant_id;
+ int rc;
+
+ /*
+ * Fvco = Fref * (DIVN + frac/2^32) with Fref = xtal_freq << doubler,
+ * so every freq_set and phase_adjust path divides by a rate derived
+ * from the XO feeding XIN/XO_CLK. The rate normally comes from a
+ * "clocks" phandle (clock-names = "xtal"). On platforms where the
+ * firmware does not expose the XO through the clock framework, fall
+ * back to a "clock-frequency" device property.
+ */
+ xtal_clk = devm_clk_get_optional_enabled(sitdev->dev, "xtal");
+ if (IS_ERR(xtal_clk))
+ return dev_err_probe(sitdev->dev, PTR_ERR(xtal_clk),
+ "Failed to get xtal clock\n");
+ sitdev->xtal_freq = clk_get_rate(xtal_clk);
+ if (!sitdev->xtal_freq) {
+ u32 freq;
+
+ if (!device_property_read_u32(sitdev->dev, "clock-frequency",
+ &freq))
+ sitdev->xtal_freq = freq;
+ }
+ if (!sitdev->xtal_freq)
+ return dev_err_probe(sitdev->dev, -EINVAL,
+ "no xtal rate: provide clocks=<&xo> + clock-names=\"xtal\", or a clock-frequency property\n");
+
+ /*
+ * Held deasserted, never pulsed: the chip configuration comes from
+ * efuse or an NVM blob applied before probe, and a reset would
+ * discard it. Must precede the first I2C access, as a board that
+ * powers up asserted keeps the chip unreachable until released.
+ */
+ sitdev->reset_gpio = devm_gpiod_get_optional(sitdev->dev, "reset",
+ GPIOD_OUT_LOW);
+ if (IS_ERR(sitdev->reset_gpio))
+ return dev_err_probe(sitdev->dev, PTR_ERR(sitdev->reset_gpio),
+ "Failed to request reset gpio\n");
+ if (sitdev->reset_gpio)
+ fsleep(10000); /* internal boot after release */
+
+ rc = sit9531x_read_variant_id(sitdev, &variant_id);
+ if (rc)
+ return rc;
+
+ sitdev->info = sit9531x_match_variant(variant_id);
+ if (!sitdev->info)
+ return dev_err_probe(sitdev->dev, -ENODEV,
+ "Unknown variant ID: 0x%02x\n",
+ variant_id);
+
+ sitdev->clock_id = sit9531x_derive_clock_id(sitdev);
+ sitdev->intsync_src = -1;
+
+ rc = devm_mutex_init(sitdev->dev, &sitdev->multiop_lock);
+ if (rc)
+ return dev_err_probe(sitdev->dev, rc,
+ "Failed to initialize mutex\n");
+
+ 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[] = {
+ { .compatible = "sitime,sit95316" },
+ { .compatible = "sitime,sit95317" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, sit9531x_of_match);
+
+/*
+ * The poll worker is not freezable and would keep issuing paged I2C
+ * transfers into a suspended adapter, where i2c_transfer() fails and a
+ * tick landing mid-suspend could tear a paged sequence between the
+ * page-selector write and the register access. Park the worker (and
+ * the IRQ that kicks it) across suspend and take a fresh sample on
+ * resume.
+ */
+static int sit9531x_suspend(struct device *dev)
+{
+ struct sit9531x_dev *sitdev = dev_get_drvdata(dev);
+
+ if (sitdev->irq > 0)
+ disable_irq(sitdev->irq);
+ kthread_cancel_delayed_work_sync(&sitdev->work);
+
+ return 0;
+}
+
+static int sit9531x_resume(struct device *dev)
+{
+ struct sit9531x_dev *sitdev = dev_get_drvdata(dev);
+
+ if (sitdev->irq > 0)
+ enable_irq(sitdev->irq);
+ kthread_queue_delayed_work(sitdev->kworker, &sitdev->work, 0);
+
+ return 0;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(sit9531x_pm_ops,
+ sit9531x_suspend, sit9531x_resume);
static struct i2c_driver sit9531x_i2c_driver = {
.driver = {
.name = "sit9531x",
.of_match_table = sit9531x_of_match,
+ .pm = pm_sleep_ptr(&sit9531x_pm_ops),
},
.probe = sit9531x_i2c_probe,
};
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 230b21b9e238..3a12e1c6b330 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -15,6 +15,8 @@
#include <linux/gpio/consumer.h>
#include <linux/i2c.h>
+#include <linux/kthread.h>
+#include <linux/list.h>
#include <linux/mutex.h>
#include <linux/regmap.h>
#include <linux/types.h>
@@ -25,6 +27,8 @@
#define SIT9531X_MAX_INPUTS 8
#define SIT9531X_NUM_INPUT_PAIRS (SIT9531X_MAX_INPUTS / 2)
#define SIT9531X_MAX_OUTPUTS 12
+/* out_pll_map[] entry meaning "this output is not routed to any PLL" */
+#define SIT9531X_OUT_PLL_UNMAPPED 0xFF
/*
* INTSYNC (the inter-PLL sync net) is modeled as two pins. The
* destination PLL that locks to INTSYNC sees an input pin
@@ -34,6 +38,15 @@
*/
#define SIT9531X_INTSYNC_PIN_ID (SIT9531X_MAX_INPUTS + 1)
#define SIT9531X_INTSYNC_OUT_PIN_ID SIT9531X_MAX_OUTPUTS
+#define SIT9531X_STATUS_POLL_MS 500
+
+/* selected_ref value when the active source is not a registered input */
+#define SIT9531X_REF_INVALID 0xFF
+
+/* SiTime IEEE OUI for EUI-64 generation */
+#define SIT9531X_OUI 0x0090C2FFFEULL
+
+struct sit9531x_dpll;
/*
* struct sit9531x_chip_info - chip variant identification
@@ -64,37 +77,98 @@ 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)
* @label: board label from DT or default
* @sig_mode: signal mode of the pair this lane belongs to
* (detected from CLKINx_INPUT_MODE at probe)
*/
struct sit9531x_ref {
- u32 freq;
- const char *label;
+ u64 freq;
+ bool enabled;
+ u8 pll_mask;
+ const char *label;
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 pair that
+ * speaks for it is the SE one, not the differential
+ * @routed: output is mapped to @pll_idx by the initial
+ * configuration; an unrouted output has no DPLL pin
+ * @pll_idx: PLL driving this output (0-3)
* @label: board label from DT or default
*/
struct sit9531x_out {
- u32 freq;
+ u64 freq;
+ bool enabled;
+ bool cmos;
+ bool routed;
+ u8 pll_idx;
const char *label;
};
+/*
+ * struct sit9531x_chan - per-PLL channel state
+ * @active: PLL has reached its active state; a PLL the loaded
+ * configuration leaves unused never does, and its
+ * loss-of-lock bit stays clear because nothing drives it
+ * @locked: PLL is locked (raw status register bit)
+ * @mode: 0 = sync (outer loop enabled), 1 = free-run
+ * @selected_ref: logical input index of the currently selected
+ * reference (the INTSYNC net maps to
+ * SIT9531X_INTSYNC_PIN_ID), or SIT9531X_REF_INVALID
+ * when the hardware source encoding is reserved
+ * @inner_lol: PLL inner loop loss-of-lock detected
+ * @ho_freeze: holdover freeze active
+ * @ho_valid: holdover memory acquired, i.e. the holdover window
+ * holds a valid estimate to fall back on
+ * @prio_mask: bit per hardware source code present in this PLL's
+ * priority table, i.e. the sources it may select. Read
+ * back from the table by the periodic worker and
+ * refreshed by every table write, so it tracks the
+ * hardware rather than the driver's intent
+ */
+struct sit9531x_chan {
+ bool active;
+ bool locked;
+ u8 mode;
+ u8 selected_ref;
+ bool inner_lol;
+ bool ho_freeze;
+ bool ho_valid;
+ u16 prio_mask;
+};
+
/*
* struct sit9531x_dev - SiT9531x device instance
- * @dev: parent device
- * @client: I2C client
- * @regmap: paged register map
* @info: detected chip variant info
- * @multiop_lock: serializes multi-register sequences
+ * @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
+ * @pll_fvco: optional per-PLL VCO in Hz from DT
+ * "sitime,pll-fvco"; 0 means derive from DIVN
+ * @out_pll_map: optional per-output source PLL (0-3, 0xff =
+ * unmapped) from DT "sitime,output-pll-map"
+ * @out_pll_map_valid: true when out_pll_map[] was populated from DT;
+ * false means use the chip's OUT_MAP registers
+ * @intsync_src: PLL index currently sourcing inter-PLL
+ * synchronization (INTSYNC), or -1 when disabled
*/
struct sit9531x_dev {
struct device *dev;
@@ -105,13 +179,83 @@ 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;
+
+ /* Optional DT board-config overrides */
+ u64 pll_fvco[SIT9531X_NUM_PLLS];
+ u8 out_pll_map[SIT9531X_MAX_OUTPUTS];
+ bool out_pll_map_valid;
+
+ /* Inter-PLL synchronization state */
+ s8 intsync_src;
+
};
+extern const struct regmap_config sit9531x_regmap_config;
+
+/* ---- Core lifecycle ---- */
+int sit9531x_dev_probe(struct sit9531x_dev *sitdev);
+int sit9531x_dev_start(struct sit9531x_dev *sitdev);
+void sit9531x_dev_stop(struct sit9531x_dev *sitdev);
+
+/* ---- Register access ---- */
+int sit9531x_read_u8(struct sit9531x_dev *sitdev, unsigned int reg,
+ u8 *val);
+int sit9531x_write_u8(struct sit9531x_dev *sitdev, unsigned int reg,
+ u8 val);
+int sit9531x_read_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 offset, u8 *val);
+int sit9531x_write_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 offset, u8 val);
+int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 offset, u8 mask, u8 val);
+
+/* ---- Input enable/disable ---- */
+
+/* ---- 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, ...).
@@ -137,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..63c72bba51bf
--- /dev/null
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -0,0 +1,450 @@
+// 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->mode) {
+ /*
+ * Free-run: the outer loop is disabled, so the PLL tracks no
+ * reference at all and its loss-of-lock bit means nothing.
+ * That is what UNLOCKED describes -- "not yet locked to any
+ * valid input (or was forced by user)".
+ */
+ *status = DPLL_LOCK_STATUS_UNLOCKED;
+ } else if (chan->locked) {
+ /*
+ * HO_ACQ is locked *and* holdover memory acquired, so it needs
+ * the holdover-valid bit rather than following from the lock.
+ */
+ if (chan->ho_valid)
+ *status = DPLL_LOCK_STATUS_LOCKED_HO_ACQ;
+ else
+ *status = DPLL_LOCK_STATUS_LOCKED;
+ } else if (chan->ho_freeze) {
+ *status = DPLL_LOCK_STATUS_HOLDOVER;
+ } else {
+ *status = DPLL_LOCK_STATUS_UNLOCKED;
+ }
+
+ /* Report inner LOL as an error condition */
+ if (status_error && chan->inner_lol)
+ *status_error = DPLL_LOCK_STATUS_ERROR_UNDEFINED;
+
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return 0;
+}
+
+/*
+ * Mode
+ * ====
+ * enum dpll_mode differentiates how a DPLL selects an input: AUTOMATIC
+ * has the device pick the highest-priority one, MANUAL has userspace
+ * request one. The device only implements the former through this
+ * driver, so AUTOMATIC is the only mode advertised.
+ *
+ * Free-run -- the outer loop disabled through PLL page reg 0x31[5] -- is
+ * not a mode in those terms, because no input is selected either way. It
+ * is reported through lock status instead, and reached through the
+ * chip-specific tool rather than over netlink.
+ *
+ * The device could implement real MANUAL: MISCINNER_PLL (PLL page reg
+ * 0x18) bit 5 switches a PLL from priority-based to manual active select,
+ * and GPIO_INPUT_FUNC_CTRL5..8 (page 0, regs 0xE8-0xEB) bit 4 makes the
+ * choice come from the register's own low nibble instead of the GPIO
+ * pins, which pins one reference while the loop keeps running. Wiring
+ * that up would let .state_on_dpll_set() accept CONNECTED; it needs bench
+ * validation first, and reg 0x18 carries GUI-generated configuration in
+ * its other bits, so it is left out until then. Advertising MANUAL and
+ * then refusing the one request MANUAL exists for is the worse of the two
+ * incomplete answers, and after merge it would be ABI.
+ */
+static int
+sit9531x_dpll_mode_get(const struct dpll_device *dpll, void *dpll_priv,
+ enum dpll_mode *mode, struct netlink_ext_ack *extack)
+{
+ *mode = DPLL_MODE_AUTOMATIC;
+
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_mode_set - put the PLL in automatic selection mode
+ *
+ * Clears the outer loop disable bit (PLL page reg 0x31[5]) and triggers a
+ * small update via reg 0x0F, so a PLL left free-running by the loaded
+ * configuration or by the chip-specific tool returns to selecting its
+ * reference from the priority table. Any other mode is refused.
+ */
+static int
+sit9531x_dpll_mode_set(const struct dpll_device *dpll, void *dpll_priv,
+ enum dpll_mode mode, struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ int rc, restore_rc;
+ u8 status;
+
+ if (mode != DPLL_MODE_AUTOMATIC) {
+ NL_SET_ERR_MSG(extack,
+ "Device selects its reference by priority; only automatic mode is supported");
+ return -EOPNOTSUPP;
+ }
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ /*
+ * Read before writing. Automatic is the only mode this driver
+ * advertises, so userspace setting it again is a no-op the device
+ * must not feel: without this, a redundant set followed by one
+ * failed latch would restore an outer-loop disable the PLL never
+ * had and put a running loop into free-run.
+ */
+ rc = sit9531x_read_pll_u8(sitdev, sitdpll->id,
+ SIT9531X_PLL_REG_STATUS, &status);
+ if (rc) {
+ NL_SET_ERR_MSG(extack, "Failed to read PLL mode register");
+ goto unlock;
+ }
+ if (!(status & SIT9531X_PLL_STATUS_OUTER_DIS)) {
+ sitdev->chan[sitdpll->id].mode = 0;
+ goto unlock;
+ }
+
+ rc = sit9531x_update_pll_u8(sitdev, sitdpll->id,
+ SIT9531X_PLL_REG_STATUS,
+ SIT9531X_PLL_STATUS_OUTER_DIS, 0);
+ if (rc) {
+ NL_SET_ERR_MSG(extack, "Failed to write PLL mode register");
+ goto unlock;
+ }
+
+ /* Trigger small update to apply without full NVM cycle */
+ rc = sit9531x_write_pll_u8(sitdev, sitdpll->id,
+ SIT9531X_PLL_REG_SMALL_UPDATE,
+ SIT9531X_SMALL_UPDATE_CMD);
+ if (rc) {
+ u8 dis = SIT9531X_PLL_STATUS_OUTER_DIS;
+
+ /*
+ * The cleared disable bit was never latched. Put it back so
+ * the register does not read "outer loop enabled" for a loop
+ * that is still free-running, which the poll would then hand
+ * to the lock-status getter as a live mode.
+ */
+ restore_rc = sit9531x_update_pll_u8(sitdev, sitdpll->id,
+ SIT9531X_PLL_REG_STATUS,
+ dis, dis);
+ if (restore_rc)
+ dev_warn(sitdev->dev,
+ "PLL%c outer loop left enabled without a latch: %d\n",
+ 'A' + sitdpll->id, restore_rc);
+ NL_SET_ERR_MSG(extack, "Failed to trigger small update");
+ goto unlock;
+ }
+
+ /*
+ * Keep the cached mode in step with the register. The periodic
+ * monitor refreshes it too, but the pin state getters and lock status
+ * read this cache and would otherwise keep reporting free-run until
+ * the next poll.
+ */
+ sitdev->chan[sitdpll->id].mode = 0;
+
+unlock:
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return rc;
+}
+
+static int
+sit9531x_dpll_supported_modes_get(const struct dpll_device *dpll,
+ void *dpll_priv, unsigned long *modes,
+ struct netlink_ext_ack *extack)
+{
+ __set_bit(DPLL_MODE_AUTOMATIC, modes);
+
+ return 0;
+}
+
+const struct dpll_device_ops sit9531x_dpll_device_ops = {
+ .lock_status_get = sit9531x_dpll_lock_status_get,
+ .mode_get = sit9531x_dpll_mode_get,
+ .mode_set = sit9531x_dpll_mode_set,
+ .supported_modes_get = sit9531x_dpll_supported_modes_get,
+ /* temp_get not available -- SiT9531x has no on-die temp sensor */
+};
+
+/*
+ * Pin-state contract
+ * ==================
+ * The five pin ops tables below fall into three roles, and only the first
+ * has a selection state machine. Each state_on_dpll callback implements
+ * the rules for its role and nothing else, so the tables cannot drift
+ * apart the way five independent encodings of this did.
+ *
+ * SELECTION role -- physical input pins, INTSYNC destination pin.
+ * Where does this reference sit in this DPLL's selection process?
+ * Predicates, all evaluated under multiop_lock:
+ * M source is present in THIS PLL's hardware priority table
+ * A chan->mode == 0 (outer loop running)
+ * L chan->locked && !chan->inner_lol (only meaningful when A)
+ * S chan->selected_ref == this pin's id
+ * get:
+ * CONNECTED A && L && S && M
+ * SELECTABLE M && !(A && L && S)
+ * DISCONNECTED !M
+ * set:
+ * DISCONNECTED remove from this PLL's table; a physical input also
+ * releases this DPLL's claim and powers the shared
+ * receiver down on the last release
+ * SELECTABLE add to this PLL's table; a physical input powers the
+ * receiver up and takes the claim, in that order
+ * CONNECTED -EOPNOTSUPP -- the device selects by priority and has
+ * no mode that pins one reference (see mode_set())
+ * other -EINVAL
+ *
+ * A is what separates CONNECTED from SELECTABLE: chan->locked is derived
+ * from the outer loss-of-lock bit, which can read clear while the outer
+ * loop is disabled, so a free-running PLL would otherwise claim its
+ * reference as the active input of a loop that is ignoring it.
+ *
+ * M is read from the hardware priority table, not from ref->pll_mask.
+ * pll_mask is only the shared-receiver refcount and says nothing about
+ * one DPLL's eligibility; it also drifts, because .prio_set writes the
+ * table without touching it. Signal quality is reported through the
+ * pin's own attributes rather than by demoting the state, so a source
+ * that is momentarily in LOS stays selectable.
+ *
+ * DRIVE role -- output pins, INTSYNC source pin.
+ * Is this pin or net being driven? Nothing is selected here, so:
+ * CONNECTED pin or net is driven
+ * DISCONNECTED pin is muted (Hi-Z), or this PLL does not drive it
+ * SELECTABLE -EINVAL on set, never reported by get
+ *
+ * FIXED role -- XO pin. Always CONNECTED; it cannot be routed.
+ */
+
+static int
+sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_direction *direction,
+ struct netlink_ext_ack *extack)
+{
+ *direction = DPLL_PIN_DIRECTION_INPUT;
+ return 0;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
+ .direction_get = sit9531x_dpll_input_pin_direction_get,
+};
+
+/*
+ * INTSYNC pin ops
+ *
+ * INTSYNC is the chip's inter-PLL sync net: one PLL drives it and other
+ * PLLs may lock to it instead of to an external reference. The two
+ * roles are exposed as two separate pins so neither overloads the other:
+ *
+ * - a source (output) pin registered on every DPLL. Connecting it on a
+ * DPLL makes that DPLL drive INTSYNC; only one DPLL may drive it at a
+ * time. It has no priority ops -- driving the net is not a reference
+ * selection.
+ * - a destination (input) pin registered on every DPLL. Connecting it
+ * on a DPLL makes that DPLL eligible to lock to INTSYNC as a
+ * reference, so it carries the priority ops.
+ */
+
+/* ---- INTSYNC source (output) pin ---- */
+
+/* The INTSYNC source pin is an output; its direction_get is defined below. */
+static int
+sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_direction *direction,
+ struct netlink_ext_ack *extack);
+
+/* ---- INTSYNC destination (input) pin ---- */
+
+/*
+ * XO (crystal oscillator) pin ops
+ *
+ * The XO is the chip's internal reference oscillator that feeds every
+ * PLL. It is exposed so userspace can see the on-chip reference, but it
+ * cannot be routed or disconnected, so it is reported permanently
+ * connected and offers no state_on_dpll_set / prio ops.
+ */
+
+static int
+sit9531x_dpll_xo_pin_state_on_dpll_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state *state,
+ struct netlink_ext_ack *extack)
+{
+ *state = DPLL_PIN_STATE_CONNECTED;
+ return 0;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_xo_pin_ops = {
+ .direction_get = sit9531x_dpll_input_pin_direction_get,
+ .state_on_dpll_get = sit9531x_dpll_xo_pin_state_on_dpll_get,
+};
+
+static int
+sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_direction *direction,
+ struct netlink_ext_ack *extack)
+{
+ *direction = DPLL_PIN_DIRECTION_OUTPUT;
+ return 0;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_output_pin_ops = {
+ .direction_get = sit9531x_dpll_output_pin_direction_get,
+};
+
+const struct dpll_pin_ops *
+sit9531x_dpll_pin_ops_get(const struct sit9531x_dpll_pin *pin)
+{
+ if (!sit9531x_dpll_is_input_pin(pin))
+ return &sit9531x_dpll_output_pin_ops;
+ if (sit9531x_dpll_is_xo_pin(pin))
+ return &sit9531x_dpll_xo_pin_ops;
+ return &sit9531x_dpll_input_pin_ops;
+}
+
+/*
+ * sit9531x_dpll_changes_check - check for state changes and notify
+ *
+ * Called from sit9531x_dev_periodic_work(). Compares current hardware
+ * state against cached values and sends netlink notifications on changes.
+ */
+void sit9531x_dpll_changes_check(struct sit9531x_dpll *sitdpll)
+{
+ 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;
+
+ /*
+ * 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;
+
+ if (state != pin->pin_state) {
+ dev_dbg(sitdev->dev, "%s state changed: %u->%u\n",
+ pin->label, pin->pin_state, state);
+ pin->pin_state = state;
+ dpll_pin_change_ntf(pin->dpll_pin);
+ }
+ }
+}
diff --git a/drivers/dpll/sit9531x/dpll.h b/drivers/dpll/sit9531x/dpll.h
new file mode 100644
index 000000000000..d6ef799d52c2
--- /dev/null
+++ b/drivers/dpll/sit9531x/dpll.h
@@ -0,0 +1,66 @@
+/* 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;
+};
+
+/* 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..60495b6e0bd5 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -37,7 +37,225 @@
*/
#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)
+
+/*
+ * Output driver configuration. Either CMOS enable means the output
+ * is wired single-ended -- one lane, or both driven as CMOS; with
+ * neither set it is a differential pair.
+ */
+#define SIT9531X_OUT_MISC0_BASE 0x1E
+#define SIT9531X_OUT_MISC0_STRIDE 0x10
+#define SIT9531X_OUT_CMOS_ENP BIT(3)
+#define SIT9531X_OUT_CMOS_ENN BIT(2)
+
+/*
+ * Output divider registers in Pages 3/4. Each output has a 34-bit
+ * integer divider mapped to 5 bytes (LSB at base reg, MSB at base-4).
+ * Outputs 0-5 are on Page 3, outputs 6-11 are on Page 4.
+ *
+ * The base register for slot N within a page is:
+ * clkout_odr_divn_base[slot] = { 0x14, 0x24, 0x34, 0x44, 0x54, 0x64 }
+ *
+ * Layout: base=LSB, base-1, base-2, base-3, base-4[1:0]=MSB.
+ *
+ * Per-chip clkout_map[] translates output index to slot position.
+ */
+#define SIT9531X_PAGE_OUTSYS0_SLOT_MAX 5 /* slots 0-5 on Page 0x03 */
+
+/* Misc output system registers */
+#define SIT9531X_REG_PRG_DIR_GEN SIT9531X_REG(0x03, 0x0F)
+#define SIT9531X_PRG_CMD_STATE 0x01
+#define SIT9531X_UPDATE_NVM 0x10
+#define SIT9531X_LOOP_LOCK 0x40
+
+/* Debug register (same offset, per-page) */
+#define SIT9531X_REG_OUTSYS_DEBUG SIT9531X_REG(0x03, 0xBD)
+#define SIT9531X_DEBUG_UNLOCK_VAL 0xC3
+
+/*
+ * On-demand phase-flush fired from a register rather than a GPIO pin.
+ * DIVO_PHASE_SEL_REG selects the in-register trigger source and
+ * DIVO_PHASE_TRIG flushes the output phase when pulsed high then low.
+ * The unrelated OEb trigger pair in bits [7:6] must be preserved.
+ */
+#define SIT9531X_REG_GPIO_FUNC_CTRL1 SIT9531X_REG(0x00, 0x65)
+#define SIT9531X_DIVO_PHASE_SEL_REG BIT(5)
+#define SIT9531X_DIVO_PHASE_TRIG BIT(4)
+
+/* ---- PLL page registers (apply to pages 0x0A-0x0D) ---- */
+#define SIT9531X_PLL_REG_SMALL_UPDATE 0x0F
+
+/*
+ * Loop-filter coefficients on PLL_PAGE regs 0x10-0x15 (3 normal +
+ * 3 fast-lock) are GUI/NVM-generated by the timing configurator and must not be
+ * reprogrammed at runtime; the register map flags them as
+ * "GUI generated configuration should not change manually".
+ */
+
+#define SIT9531X_PLL_REG_OUT_MAP_HI 0x27
+#define SIT9531X_PLL_REG_OUT_MAP_LO 0x28
+#define SIT9531X_PLL_REG_STATUS 0x31
+
+#define SIT9531X_PLL_REG_ACTIVE 0x02
+#define SIT9531X_PLL_ACTIVE_BIT BIT(0) /* PLL reached active state */
+
+#define SIT9531X_PLL_STATUS_OUTER_DIS BIT(5)
+
+/*
+ * Per-PLL status register. HO_VALID says the holdover window holds a
+ * valid frequency estimate, i.e. holdover memory has been acquired; it is
+ * not the same as HO_FREEZE (page 0, reg 0x0A), which says the PLL has
+ * already switched over to holdover.
+ */
+#define SIT9531X_PLL_REG_STATUS_1 0x06
+#define SIT9531X_PLL_STATUS_1_HO_VALID BIT(2)
+
+#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] 28+ messages in thread* Re: [PATCH v10 05/14] dpll: sit9531x: register DPLL devices and pins
2026-09-21 20:11 ` [PATCH v10 05/14] dpll: sit9531x: register DPLL devices and pins Ali Rouhi
@ 2026-09-26 2:34 ` Jakub Kicinski
0 siblings, 0 replies; 28+ messages in thread
From: Jakub Kicinski @ 2026-09-26 2:34 UTC (permalink / raw)
To: arouhi
Cc: Jakub Kicinski, jiri, vadim.fedorenko, arkadiusz.kubalewski,
ivecera, robh, krzk+dt, conor+dt, cjubran, pabeni,
Oleg.Zadorozhnyi, prabhakar.mahadev-lad.rj, dev, devicetree,
netdev, linux-kernel
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
dpll: sit9531x: register DPLL devices and pins
This patch registers each of the four SiT9531x PLLs as a DPLL device.
Each device gets lock status and mode callbacks, and the crystal is
registered as a permanently connected input pin on each one.
A kthread worker polls PLL and input state twice a second into a cache.
It notifies the DPLL core when something changes, and the optional INTRB
interrupt can kick it early.
The clock_id is built from the chip variant, the I2C address and the low
eight bits of the adapter number.
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index cf1cda87fa52c..6f19dab89beeb 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
[ ... ]
> +/*
> + * Report whether a slot is currently forced into Hi-Z, i.e. the driver
> + * (or the blob) took control of the Hi-Z state (MASK bit set) and drives
> + * it low (STATE bit clear). Either register pair muting the slot counts,
> + * mirroring what sit9531x_output_disable() programs.
> + */
> +static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev,
> + u8 out_idx, bool *muted)
[ ... ]
> + * Read the pair that belongs to the way this output is wired.
> + * Testing both and taking either as proof of a mute answers
> + * from a register nothing drives, and disagrees with itself
> + * when a mute lands on one pair and fails on the other.
> */
[Severity: Low]
This isn't a bug, but several comments added in this patch don't match
the code.
The header comment says that either register pair muting the slot counts.
The body reads only the pair chosen by out->cmos, and its own comment
rejects the either-pair approach. Also, sit9531x_output_disable() does not
exist yet in this commit.
Some others in the same patch:
sit9531x_ref_state_fetch() is documented as "Reads LOS and OOF status for
the given input lane from the Page 0x06 clock monitor registers". It
actually reads the Page 0x02 force/state registers through
sit9531x_input_get_regs(), and it only sets ref->enabled.
The kernel-doc blocks for sit9531x_chan_state_fetch and sit9531x_dev_start
sit above sit9531x_pll_is_active() and sit9531x_eeprom_state_report(). They
end up documenting the wrong functions.
In struct sit9531x_chan, @locked is described as "PLL is locked (raw status
register bit)". sit9531x_chan_state_fetch() computes it as
active && !outer_lol. The new irq_ack_fails member of struct sit9531x_dev
has no kernel-doc entry.
The "Pin-state contract" comment in dpll.c describes five pin ops tables
with get/set rules. This commit has three tables and no set callbacks.
The commit message says:
Output state is not polled: it changes only through a request, and
the getter reads the device again when one could not be confirmed.
This patch has no output state getter and no state_stale handling. Both
arrive with the output-state patch later in the series.
Several constants and fields are not used in this commit:
SIT9531X_REG_XO2_GENERIC, the SIT9531X_FVCO_* bands, SIT9531X_DIVO_MAX,
the INTSYNC, debug and GPIO_FUNC_CTRL1 registers,
SIT9531X_OUT_PLL_UNMAPPED, and intsync_src. That doesn't match this note in
the v10 changelog:
declared each helper in the patch that defines it.
The mismatches in sit9531x_ref_state_fetch() and
sit9531x_output_forced_hiz(), and the misplaced kernel-doc blocks, are
still there at the end of the series.
[ ... ]
> +struct sit9531x_dpll *sit9531x_dpll_alloc(struct sit9531x_dev *sitdev, u8 ch)
> +{
> + struct sit9531x_dpll *sitdpll;
> +
> + sitdpll = kzalloc_obj(*sitdpll, GFP_KERNEL);
> + if (!sitdpll)
> + return ERR_PTR(-ENOMEM);
> +
> + sitdpll->dev = sitdev;
> + sitdpll->id = ch;
> + sitdpll->lock_status = DPLL_LOCK_STATUS_UNLOCKED;
> + INIT_LIST_HEAD(&sitdpll->pins);
[Severity: Low]
Should lock_status_error be initialized here as well?
It stays at 0 from kzalloc_obj(), which is not a valid enum
dpll_lock_status_error value; DPLL_LOCK_STATUS_ERROR_NONE is 1.
sit9531x_dpll_lock_status_get() always writes at least
DPLL_LOCK_STATUS_ERROR_NONE, so this test in
sit9531x_dpll_changes_check() is true on the first tick:
if (sitdpll->lock_status != lock_status ||
sitdpll->lock_status_error != status_error) {
As a result, every DPLL sends dpll_device_change_ntf() right after probe,
even when nothing has changed.
pin->pin_state has the same problem. It starts at 0, while
DPLL_PIN_STATE_CONNECTED is 1. Once input pins are registered later in
the series, each polled pin would send a spurious dpll_pin_change_ntf() on
the first tick. Both fields are still uninitialized at the end of the
series.
[ ... ]
> +static u64 sit9531x_derive_clock_id(struct sit9531x_dev *sitdev)
> +{
> + u64 clkid;
> +
> + clkid = SIT9531X_OUI << 24;
> + clkid |= (u64)(i2c_adapter_id(sitdev->client->adapter) & 0xff) << 16;
[Severity: Medium]
The commit message says:
That needs a machine with more than 256 I2C adapters and the same
part twice at the same address on two of them
Is that accurate? Bus numbers are not limited by the number of adapters.
i2c_add_adapter() takes the bus number from the DT "i2c" alias through
of_alias_get_id(). i2c_init() then sets __i2c_first_dynamic_bus_num to the
highest alias + 1. That means a board with only a few adapters can still
have both bus 0 and bus 256.
Two parts of the same variant at the same address on those buses would get
the same clock_id. dpll_device_get() and dpll_pin_get() match on clock_id,
index and module. The second chip would therefore be folded onto the first
chip's DPLL and pin objects.
Could the commit message be corrected, or could the identifier be built so
that the adapter number is not truncated?
[ ... ]
> diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
> index 230b21b9e2381..3a12e1c6b3309 100644
> --- a/drivers/dpll/sit9531x/core.h
> +++ b/drivers/dpll/sit9531x/core.h
[ ... ]
> @@ -105,13 +179,83 @@ struct sit9531x_dev {
[ ... ]
> + /* Optional DT board-config overrides */
> + u64 pll_fvco[SIT9531X_NUM_PLLS];
> + u8 out_pll_map[SIT9531X_MAX_OUTPUTS];
> + bool out_pll_map_valid;
[Severity: Low]
This isn't a bug, but nothing in this commit reads the "sitime,pll-fvco"
or "sitime,output-pll-map" properties. In this commit
sit9531x_out_state_fetch() takes routing only from the OUT_MAP registers.
These fields and their kernel-doc describe behaviour that only arrives
with the last patch in the series, "dpll: sit9531x: allow the device tree
to override two board facts". Could they be moved into that patch?
[ ... ]
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> new file mode 100644
> index 0000000000000..63c72bba51bfe
> --- /dev/null
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -0,0 +1,450 @@
[ ... ]
> +static int
> +sit9531x_dpll_lock_status_get(const struct dpll_device *dpll, void *dpll_priv,
[ ... ]
> + } 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;
[Severity: Medium]
Should an inner loss of lock affect the reported status here?
chan->locked only reflects active && !outer_lol. With chan->inner_lol set
and the outer LOL bit clear, this function reports
DPLL_LOCK_STATUS_LOCKED or DPLL_LOCK_STATUS_LOCKED_HO_ACQ. The core then
drops the error for those states:
drivers/dpll/dpll_netlink.c:dpll_msg_add_lock_status() {
...
if (status_error &&
(status == DPLL_LOCK_STATUS_UNLOCKED ||
status == DPLL_LOCK_STATUS_HOLDOVER) &&
nla_put_u32(msg, DPLL_A_LOCK_STATUS_ERROR, status_error))
return -EMSGSIZE;
...
}
So userspace would see a locked DPLL while the inner loop is unlocked.
The pin-state contract comment later in this file defines L as
chan->locked && !chan->inner_lol, which disagrees with this getter.
In the same case, sit9531x_dpll_changes_check() sends
dpll_device_change_ntf() when only the error changes. With a locked status
that notification carries no visible change.
[ ... ]
> + * The device could implement real MANUAL: MISCINNER_PLL (PLL page reg
> + * 0x18) bit 5 switches a PLL from priority-based to manual active select,
> + * and GPIO_INPUT_FUNC_CTRL5..8 (page 0, regs 0xE8-0xEB) bit 4 makes the
> + * choice come from the register's own low nibble instead of the GPIO
> + * pins, which pins one reference while the loop keeps running. Wiring
[ ... ]
> + */
> +static int
> +sit9531x_dpll_mode_get(const struct dpll_device *dpll, void *dpll_priv,
> + enum dpll_mode *mode, struct netlink_ext_ack *extack)
> +{
> + *mode = DPLL_MODE_AUTOMATIC;
> +
> + return 0;
> +}
[Severity: Medium]
What happens if the loaded profile already has a PLL in manual active
select?
The comment above describes two such controls: MISCINNER_PLL bit 5, and
bit 4 of GPIO_INPUT_FUNC_CTRL5..8, either of which can pin a reference.
Probe never reads or resets either of them, and
sit9531x_dpll_mode_get() always returns DPLL_MODE_AUTOMATIC.
A profile that uses manual selection would therefore be reported as
automatic. Requesting DPLL_MODE_AUTOMATIC would not fix that: the core
skips same-mode requests, and sit9531x_dpll_mode_set() only clears
SIT9531X_PLL_STATUS_OUTER_DIS.
Would it make sense to read reg 0x18 bit 5 here, or at least warn at probe
when it is set?
> +
> +/*
> + * sit9531x_dpll_mode_set - put the PLL in automatic selection mode
[ ... ]
> +static int
> +sit9531x_dpll_mode_set(const struct dpll_device *dpll, void *dpll_priv,
> + enum dpll_mode mode, struct netlink_ext_ack *extack)
> +{
[ ... ]
> + if (!(status & SIT9531X_PLL_STATUS_OUTER_DIS)) {
> + sitdev->chan[sitdpll->id].mode = 0;
> + goto unlock;
> + }
> +
> + rc = sit9531x_update_pll_u8(sitdev, sitdpll->id,
> + SIT9531X_PLL_REG_STATUS,
> + SIT9531X_PLL_STATUS_OUTER_DIS, 0);
[Severity: Medium]
Can sit9531x_dpll_mode_set() ever be reached?
sit9531x_dpll_mode_get() always returns DPLL_MODE_AUTOMATIC, and
sit9531x_dpll_supported_modes_get() advertises only DPLL_MODE_AUTOMATIC.
In the core:
drivers/dpll/dpll_netlink.c:dpll_mode_set() {
...
ret = ops->mode_get(dpll, dpll_priv(dpll), &old_mode, extack);
...
if (mode == old_mode)
return 0;
...
if (!test_bit(mode, modes)) {
...
}
A DPLL_MODE_AUTOMATIC request returns before ops->mode_set is called. Any
other mode is rejected by the test_bit() check.
That makes the OUTER_DIS read, the clear, the small-update latch, the
restore and the cache update look unreachable. A PLL left free-running by
the loaded profile would stay that way, while userspace gets success for
its DPLL_MODE_AUTOMATIC request. This is still the case at the end of the
series.
Should mode_get report something other than DPLL_MODE_AUTOMATIC when
OUTER_DIS is set, or should this recovery happen some other way?
[ ... ]
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH v10 06/14] dpll: sit9531x: implement input pin state on a DPLL
2026-09-21 20:11 [PATCH v10 00/14] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (4 preceding siblings ...)
2026-09-21 20:11 ` [PATCH v10 05/14] dpll: sit9531x: register DPLL devices and pins Ali Rouhi
@ 2026-09-21 20:11 ` Ali Rouhi
2026-09-26 2:34 ` Jakub Kicinski
2026-09-21 20:11 ` [PATCH v10 07/14] dpll: sit9531x: add support to get and set priority on input pins Ali Rouhi
` (7 subsequent siblings)
13 siblings, 1 reply; 28+ messages in thread
From: Ali Rouhi @ 2026-09-21 20:11 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi,
prabhakar.mahadev-lad.rj, dev, devicetree, netdev, linux-kernel,
Ali Rouhi
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Report and set whether an input feeds a given PLL. The device selects
its reference itself from a priority table, so membership of that table is
what "connected" means here: an input in the table is selectable, the one
the PLL has selected is connected, and an input absent from the table is
disconnected. Selected, not locked: how well the loop is tracking the
reference it picked is what lock status answers, and a PLL that is
following a reference it has not settled on yet is still following that
one and no other. Setting the state therefore adds the input to the
table or removes it, and the table is read back rather than remembered.
Removing the last one is allowed, and empties the table. The four-bit
slot encoding has a value for that: two of its codes address a fifth
input pair this part does not have, so a slot holding one names nothing
the device can select. A DPLL whose table is empty has no reference to
choose and no pin reporting itself selectable, which is what a request to
disconnect the last input means; lock status follows the loop from there
without the driver writing a mode.
The pins fall into three roles and only the first answers this question,
so the contract for all of them is written above the operations rather
than left to be inferred: inputs and the inter-PLL sync destination take
part in selection; an output and the sync source are driven by the PLL and
report whether they carry a signal; the crystal is fixed and always
connected, since a PLL cannot be told to stop using it.
Rewriting the table means forcing the PLL into holdover, waiting for it to
take, writing every slot and releasing holdover again, all under the
device lock -- ten to twenty milliseconds. The device has no way to
change one slot in isolation, and holdover is what keeps the loop from
chasing a table that is momentarily inconsistent, so the sequence is the
cost of doing it correctly.
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
Notes:
Changes in v10:
Priority table coherence: a table that was written only in part is put
back rather than latched; the mask describes what reached the device
rather than what was asked for; a slot naming no reference counts as
free space instead of as a source; moving a priority collapses the
duplicate entries a disconnect leaves behind, and a slot the collapsed
table cannot express is refused rather than turned into a different one.
Removing the last reference now empties the table instead of being
refused, so a DPLL can be left with no source. The slot encoding has
codes for an input pair the part does not have, which read back as no
valid reference; every slot gets one. No mode register is written.
Connected means the device has selected the pin, not that the loop has
locked to it.
The receiver is gated on its own state rather than on this DPLL's claim,
and only a receiver this request turned on is turned back off.
drivers/dpll/sit9531x/core.c | 604 ++++++++++++++++++++++++++++++++++-
drivers/dpll/sit9531x/core.h | 10 +
drivers/dpll/sit9531x/dpll.c | 193 +++++++++++
drivers/dpll/sit9531x/regs.h | 4 +
4 files changed, 800 insertions(+), 11 deletions(-)
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 6f19dab89bee..84c1f86e6d4c 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -206,6 +206,94 @@ 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;
+
+ if (rc)
+ return rc;
+
+ ref->enabled = false;
+
+ return 0;
+}
+
+/*
+ * sit9531x_input_enable - enable an input reference
+ * @index: logical input index (0-N)
+ *
+ * Clears the force mask bit for the given input, returning it to
+ * hardware default (enabled).
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_input_enable(struct sit9531x_dev *sitdev, u8 index)
+{
+ unsigned int force_reg, state_reg;
+ struct sit9531x_ref *ref;
+ u8 pair, val;
+ int rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (index >= SIT9531X_MAX_INPUTS)
+ return -EINVAL;
+
+ ref = &sitdev->ref[index];
+ pair = sit9531x_input_pair(index);
+ sit9531x_input_get_regs(sitdev, index, &force_reg, &state_reg);
+
+ rc = sit9531x_read_u8(sitdev, force_reg, &val);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, force_reg, val & ~BIT(pair));
+ if (rc)
+ return rc;
+
+ ref->enabled = true;
+
+ return 0;
+}
+
/*
* Output enable / disable (Hi-Z control)
*
@@ -368,6 +456,92 @@ static u8 sit9531x_prio_slot_get(u8 val, u8 slot)
return val >> SIT9531X_PRIO_HI_SHIFT;
}
+/* Place source @src in priority slot @slot of a register value. */
+static u8 sit9531x_prio_slot_set(u8 val, u8 slot, u8 src)
+{
+ if (slot & 1)
+ return (val & (SIT9531X_PRIO_NIBBLE_MASK <<
+ SIT9531X_PRIO_HI_SHIFT)) |
+ (src & SIT9531X_PRIO_NIBBLE_MASK);
+
+ return (val & SIT9531X_PRIO_NIBBLE_MASK) |
+ ((src & SIT9531X_PRIO_NIBBLE_MASK) <<
+ SIT9531X_PRIO_HI_SHIFT);
+}
+
+/*
+ * Commit a priority-table programming sequence through the Page-0
+ * programming directive register.
+ *
+ * A small change update is all the table needs. The NVM-bank and
+ * loop-lock directives that the output system issues do not belong
+ * here: the former programs non-volatile storage from the efuse and
+ * the latter only means anything after an escape to the PRG_CMD
+ * state. This matches the documented input_priority_sel() procedure.
+ */
+static int sit9531x_prio_prg_commit(struct sit9531x_dev *sitdev)
+{
+ int rc;
+
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+ SIT9531X_SMALL_UPDATE_CMD);
+ if (rc)
+ return rc;
+
+ usleep_range(1000, 2000);
+
+ return 0;
+}
+
+/*
+ * Fold a source code to the lane a DPLL pin actually represents.
+ *
+ * Differential input pairs expose only the P lane as a DPLL pin. A
+ * priority table entry encoded as an N lane for such a pair must map to
+ * the P-lane source for pin-facing operations (membership, priority slots,
+ * add/remove/set lookups), matching sit9531x_ref_pll_mask_fetch().
+ */
+static u8 sit9531x_prio_src_canon(const struct sit9531x_dev *sitdev, u8 src)
+{
+ u8 index = sit9531x_hw_src_input(src);
+
+ if (index >= sitdev->info->num_inputs)
+ return src;
+
+ if (sit9531x_input_is_n(index) &&
+ sitdev->ref[index].sig_mode == SIT9531X_MODE_DE)
+ return sit9531x_input_hw_src(index - 1);
+
+ return src;
+}
+
+/*
+ * sit9531x_input_prio_present - is a source listed in a PLL's priority table
+ * @input_idx: input source in hardware encoding (see
+ * sit9531x_input_hw_src())
+ *
+ * Answers from the membership mask that every table write and every poll
+ * refreshes, which is what the pin state getters test. The priority slot
+ * cannot answer this: a source that is not in the table reports the lowest
+ * slot, so the slot value alone does not separate absent from last.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+bool sit9531x_input_prio_present(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 input_idx)
+{
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return false;
+
+ input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+ if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+ return false;
+
+ return !!(sitdev->chan[pll_idx].prio_mask & BIT(input_idx));
+}
+
/*
* Rebuild a PLL's membership mask from the source codes of its priority
* table. The mask is what the pin state getters test, so it is refreshed
@@ -375,11 +549,20 @@ static u8 sit9531x_prio_slot_get(u8 val, u8 slot)
* 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)
+ const u8 *srcs, u8 written)
{
u16 mask = 0;
u8 slot;
+ /*
+ * A table written only in part is not described by the values the
+ * request carried, and there is nothing here to describe it with
+ * instead, so the mask it had stands until the next poll reads the
+ * table back.
+ */
+ if (written < SIT9531X_PRIO_MAX_SLOTS)
+ return;
+
for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
u8 src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
@@ -390,6 +573,107 @@ static void sit9531x_prio_mask_build(struct sit9531x_dev *sitdev, u8 pll_idx,
sitdev->chan[pll_idx].prio_mask = mask;
}
+/* Attempts to release a forced holdover before reporting it stuck. */
+#define SIT9531X_HO_CLEAR_TRIES 3
+
+static int sit9531x_prio_table_commit(struct sit9531x_dev *sitdev, u8 pll_idx,
+ const u8 *srcs)
+{
+ u8 val, slot, attempt, written = 0;
+ int rc = 0, prg_rc, ho_rc = 0;
+ u16 reg;
+
+ rc = sit9531x_update_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_HO_CTRL,
+ BIT(SIT9531X_PLL_HO_FORCE_BIT),
+ BIT(SIT9531X_PLL_HO_FORCE_BIT));
+ if (rc)
+ return rc;
+
+ usleep_range(10000, 12000);
+
+ /*
+ * 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 commit;
+
+ written = slot + 2;
+ }
+
+ /*
+ * The last slot shares its register with a nibble this table does
+ * not use, so that one is read first to leave it as it was.
+ */
+ reg = sit9531x_prio_reg(pll_idx, slot);
+
+ rc = sit9531x_read_u8(sitdev, reg, &val);
+ if (rc)
+ goto commit;
+
+ val = sit9531x_prio_slot_set(val, slot, srcs[slot]);
+
+ rc = sit9531x_write_u8(sitdev, reg, val);
+ if (rc)
+ goto commit;
+
+ written = SIT9531X_PRIO_MAX_SLOTS;
+
+commit:
+ /*
+ * Latch unconditionally: slots written before a failed write 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 from the table just written, so a get that
+ * follows a set does not have to wait for the next poll -- but only
+ * for the slots that reached the device. Describing a table that
+ * does not exist would make the membership test answer for writes
+ * that failed, and that test is what decides whether a failed
+ * request gets rolled back.
+ */
+ sit9531x_prio_mask_build(sitdev, pll_idx, srcs, written);
+
+ /*
+ * Release the forced holdover. Nothing else in the driver clears
+ * this bit, so a PLL left with it set reports holdover until the
+ * next table write on the same PLL happens to clear it, which may
+ * never come. Retry before giving up, and say so if it stays set.
+ */
+ for (attempt = 0; attempt < SIT9531X_HO_CLEAR_TRIES; attempt++) {
+ ho_rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_HO_CTRL,
+ BIT(SIT9531X_PLL_HO_FORCE_BIT),
+ 0);
+ if (!ho_rc)
+ break;
+ usleep_range(1000, 2000);
+ }
+ if (ho_rc) {
+ dev_err(sitdev->dev, "PLL%c left in forced holdover: %d\n",
+ 'A' + pll_idx, ho_rc);
+ if (!rc)
+ rc = ho_rc;
+ }
+
+ return rc;
+}
+
/*
* sit9531x_prio_table_read - read a PLL's priority-table source codes
* @srcs: output array of SIT9531X_PRIO_MAX_SLOTS source codes
@@ -414,6 +698,242 @@ static int sit9531x_prio_table_read(struct sit9531x_dev *sitdev, u8 pll_idx,
return 0;
}
+/*
+ * sit9531x_input_prio_set - move an input to a priority slot
+ * @input_idx: input source in hardware encoding (0-11, see
+ * sit9531x_input_hw_src())
+ * @prio: priority slot position (0 = highest)
+ *
+ * Reads the PLL's table, takes the source out of wherever it sits and
+ * reinserts it at @prio, shifting the entries in between. The rest keep
+ * their relative order: a priority change asks about one input, so the
+ * fallbacks configured behind it have to survive it.
+ *
+ * The table is what makes a source eligible for this PLL, so this only
+ * ever reorders sources already in it. A source that is absent is
+ * disconnected on this PLL, and inserting it here would make it a
+ * selection candidate again behind the caller's back; that is a connect,
+ * and it belongs to the pin's state setter.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, -EINVAL if the source is not in the table,
+ * <0 on error
+ */
+int sit9531x_input_prio_set(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 input_idx, u8 prio)
+{
+ u8 orig_srcs[SIT9531X_PRIO_MAX_SLOTS];
+ u8 rest[SIT9531X_PRIO_MAX_SLOTS];
+ u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+ u8 slot, n = 0;
+ int rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+ input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+ if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+ return -EINVAL;
+ if (prio >= SIT9531X_PRIO_MAX_SLOTS)
+ return -EINVAL;
+
+ rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+ if (rc)
+ return rc;
+
+ memcpy(orig_srcs, srcs, sizeof(srcs));
+
+ /*
+ * Take every copy of the source out first. A disconnect backfills
+ * the slots it frees with the lowest-priority source still listed,
+ * so the same source appearing more than once is an ordinary state
+ * of the table, and shifting from its first copy alone can leave
+ * another copy ahead of the slot the request named -- a priority
+ * the request did not ask for and the getter would then report.
+ */
+ for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++)
+ if (sit9531x_prio_src_canon(sitdev, srcs[slot]) != input_idx)
+ rest[n++] = srcs[slot];
+
+ if (n == SIT9531X_PRIO_MAX_SLOTS)
+ return -EINVAL;
+
+ /*
+ * Once the copies are collapsed the source can only sit behind the
+ * other sources the table lists, because the slots past the last of
+ * them repeat it. A slot beyond that is a priority the table cannot
+ * express, and placing the source at the nearest one it can would
+ * report success for a priority nobody asked for.
+ */
+ if (prio > n)
+ return -ERANGE;
+
+ for (slot = 0; slot < prio; slot++)
+ srcs[slot] = rest[slot];
+
+ srcs[prio] = input_idx;
+
+ for (slot = prio + 1; slot < SIT9531X_PRIO_MAX_SLOTS; slot++)
+ srcs[slot] = (slot - 1 < n) ? rest[slot - 1] : srcs[slot - 1];
+ if (!memcmp(srcs, orig_srcs, sizeof(srcs)))
+ return 0;
+
+ return sit9531x_prio_table_commit(sitdev, pll_idx, srcs);
+}
+
+/*
+ * sit9531x_input_prio_remove - drop an input from a PLL's priority table
+ * @input_idx: input source in hardware encoding
+ *
+ * Rewrites the priority table with the source removed: the remaining
+ * sources are compacted toward the highest-priority slots and the freed
+ * tail slots are backfilled with the lowest-priority remaining source.
+ * This makes a disconnected input ineligible for automatic reference
+ * selection, not just gated at the input buffer.
+ *
+ * Removing a source that is absent is what the caller asked for already,
+ * so it succeeds without touching the table. Removing the last one fills
+ * every slot with the code for the pair this part does not have, which
+ * leaves the device with nothing to select and no reference to fall back
+ * on -- which is what a request to disconnect the last input means.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, <0 on error
+ */
+int sit9531x_input_prio_remove(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 input_idx)
+{
+ u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+ u8 kept[SIT9531X_PRIO_MAX_SLOTS];
+ u8 slot, count = 0;
+ bool found = false;
+ int rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+ input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+
+ rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+ if (rc)
+ return rc;
+
+ for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+ if (sit9531x_prio_src_canon(sitdev, srcs[slot]) == input_idx)
+ found = true;
+ else
+ kept[count++] = srcs[slot];
+ }
+
+ if (!found)
+ return 0;
+
+ if (count == 0) {
+ /*
+ * Nothing is left to compact toward, so every slot gets the
+ * code that names no reference. The device is then left
+ * with no source to select, which is what disconnecting the
+ * last input asks for; lock status follows the loop on its
+ * own from there.
+ */
+ memset(kept, SIT9531X_PRIO_SRC_NONE, sizeof(kept));
+ } else {
+ /* Backfill freed tail slots with the lowest-priority src */
+ while (count < SIT9531X_PRIO_MAX_SLOTS) {
+ kept[count] = kept[count - 1];
+ count++;
+ }
+ }
+
+ return sit9531x_prio_table_commit(sitdev, pll_idx, kept);
+}
+
+/*
+ * sit9531x_input_prio_add - make an input eligible in a PLL's table
+ * @input_idx: input source in hardware encoding
+ *
+ * Ensures the source appears in the priority table so it can be picked
+ * by automatic reference selection again after a disconnect. If the
+ * source is already listed the table is left untouched; otherwise it
+ * replaces a duplicate at the tail. A table that contains only unique
+ * sources cannot accept a new one without evicting another, so that
+ * case fails with -ENOSPC.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 input_idx)
+{
+ u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+ u8 seen[SIT9531X_PRIO_NUM_SRC] = { 0 };
+ u8 replace = SIT9531X_PRIO_MAX_SLOTS;
+ u8 slot, src, src_canon;
+ int rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+ input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+ if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+ return -EINVAL;
+
+ rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+ if (rc)
+ return rc;
+
+ for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++)
+ if (sit9531x_prio_src_canon(sitdev, srcs[slot]) == input_idx)
+ return 0;
+
+ for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+ src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
+ src_canon = sit9531x_prio_src_canon(sitdev, src);
+ if (!sit9531x_prio_src_usable(src))
+ continue;
+
+ seen[src_canon]++;
+ }
+
+ /*
+ * A slot whose code names no usable source -- a reserved value, or
+ * one past the inputs this variant has -- is free space, and taking
+ * it costs nothing. Prefer it over displacing a real reference.
+ */
+ for (slot = SIT9531X_PRIO_MAX_SLOTS; slot-- > 0;) {
+ src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
+ if (!sit9531x_prio_src_usable(src)) {
+ replace = slot;
+ break;
+ }
+ }
+
+ /* Otherwise take the last slot holding a source listed twice. */
+ for (slot = SIT9531X_PRIO_MAX_SLOTS;
+ replace == SIT9531X_PRIO_MAX_SLOTS && slot-- > 0;) {
+ src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
+ src_canon = sit9531x_prio_src_canon(sitdev, src);
+ if (!sit9531x_prio_src_usable(src))
+ continue;
+
+ if (seen[src_canon] > 1) {
+ replace = slot;
+ break;
+ }
+ }
+
+ if (replace == SIT9531X_PRIO_MAX_SLOTS)
+ return -ENOSPC;
+
+ srcs[replace] = input_idx;
+
+ return sit9531x_prio_table_commit(sitdev, pll_idx, srcs);
+}
+
/* XO doubler register */
#define SIT9531X_REG_XO2_GENERIC SIT9531X_REG(0x00, 0x2D)
#define SIT9531X_XO_DOUBLER_ENB_BIT 7 /* inverted: 0 = enabled */
@@ -673,7 +1193,8 @@ static int sit9531x_chan_state_fetch(struct sit9531x_dev *sitdev, u8 pll_idx)
if (rc)
return rc;
- sit9531x_prio_mask_build(sitdev, pll_idx, srcs);
+ sit9531x_prio_mask_build(sitdev, pll_idx, srcs,
+ SIT9531X_PRIO_MAX_SLOTS);
/* STATUS_1_GENERIC reports loss of lock, so invert it. */
chan->active = active;
@@ -893,6 +1414,41 @@ static void sit9531x_dev_ref_states_update(struct sit9531x_dev *sitdev)
}
}
+/*
+ * 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)
{
int i, rc;
@@ -904,6 +1460,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);
}
/*
@@ -1307,14 +1865,38 @@ static void sit9531x_dpll_pins_unregister(struct sit9531x_dpll *sitdpll)
}
}
+/*
+ * sit9531x_input_pin_is_registrable - check if an input pin is registrable
+ *
+ * Split out so input-model changes stay local to this helper.
+ *
+ * Return: true if the input pin should be registered, false otherwise
+ */
+static bool sit9531x_input_pin_is_registrable(struct sit9531x_dev *sitdev,
+ u8 index)
+{
+ if (index >= sitdev->info->num_inputs)
+ return false;
+
+ /*
+ * The N lane of a differentially-configured pair is not a
+ * standalone input and is skipped (zl3073x model).
+ */
+ if (sit9531x_input_is_n(index) &&
+ sitdev->ref[index].sig_mode == SIT9531X_MODE_DE)
+ return false;
+
+ return true;
+}
+
/*
* sit9531x_dpll_pin_is_registrable - check if a pin should be registered
* @dir: pin direction
* @index: pin hardware index
*
- * Only the XO pin has a complete pin-op table in this patch, so only
- * the XO pin is registrable here. Other pin classes are registered
- * once their state callbacks land in the following patches.
+ * For input pins: delegate to sit9531x_input_pin_is_registrable().
+ * 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
*/
@@ -1322,15 +1904,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 3a12e1c6b330..033e1059bde9 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -229,8 +229,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 63c72bba51bf..4437c95f578f 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -286,6 +286,45 @@ 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.
+ *
+ * CONNECTED means the device has selected this pin, not that the loop
+ * has settled on it: how well it is tracking is what lock status answers,
+ * and a PLL following a reference it has not locked to yet is still
+ * following that reference and no other.
+ *
+ * Membership comes from chan->prio_mask, which is the priority table read
+ * back from the chip -- not a record of what the driver asked for. The
+ * getter runs on every poll for every input pin of every DPLL, so it takes
+ * the mask the worker refreshed rather than rescanning the table over I2C
+ * each time; table writes refresh it too, so a get right after a set does
+ * not report the old membership.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static void
+sit9531x_dpll_selection_state_get(struct sit9531x_dev *sitdev,
+ const struct sit9531x_dpll *sitdpll,
+ u8 pin_id, enum dpll_pin_state *state)
+{
+ const struct sit9531x_chan *chan;
+ bool active_input;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
+ active_input = !chan->mode && chan->selected_ref == pin_id;
+
+ if (!(chan->prio_mask & BIT(sit9531x_input_hw_src(pin_id))))
+ *state = DPLL_PIN_STATE_DISCONNECTED;
+ else if (active_input)
+ *state = DPLL_PIN_STATE_CONNECTED;
+ else
+ *state = DPLL_PIN_STATE_SELECTABLE;
+}
+
static int
sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
void *pin_priv,
@@ -298,8 +337,162 @@ 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_set()).
+ * Refuse instead of quietly behaving like SELECTABLE.
+ */
+ NL_SET_ERR_MSG(extack,
+ "Device selects its reference by priority; use selectable");
+ rc = -EOPNOTSUPP;
+ break;
+ case DPLL_PIN_STATE_SELECTABLE:
+ /*
+ * 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,
};
/*
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index 60495b6e0bd5..4d8eb3ceac9f 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)
--
2.43.0
^ permalink raw reply [flat|nested] 28+ messages in thread* Re: [PATCH v10 06/14] dpll: sit9531x: implement input pin state on a DPLL
2026-09-21 20:11 ` [PATCH v10 06/14] dpll: sit9531x: implement input pin state on a DPLL Ali Rouhi
@ 2026-09-26 2:34 ` Jakub Kicinski
0 siblings, 0 replies; 28+ messages in thread
From: Jakub Kicinski @ 2026-09-26 2:34 UTC (permalink / raw)
To: arouhi
Cc: Jakub Kicinski, jiri, vadim.fedorenko, arkadiusz.kubalewski,
ivecera, robh, krzk+dt, conor+dt, cjubran, pabeni,
Oleg.Zadorozhnyi, prabhakar.mahadev-lad.rj, dev, devicetree,
netdev, linux-kernel
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
dpll: sit9531x: implement input pin state on a DPLL
This patch adds state_on_dpll_get/set for SiT9531x input pins. Whether a
source is in a PLL's hardware priority table decides between DISCONNECTED
and SELECTABLE, and the reference the PLL has selected is reported as
CONNECTED. Setting the state rewrites the priority table under forced
holdover, and a per-input DPLL claim mask decides when the shared input
receiver is powered on or off.
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index 6f19dab89beeb..84c1f86e6d4cf 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -206,6 +206,94 @@ static void sit9531x_input_get_regs(const struct sit9531x_dev *sitdev,
[ ... ]
> +int sit9531x_input_disable(struct sit9531x_dev *sitdev, u8 index)
> +{
[ ... ]
> + rc = sit9531x_read_u8(sitdev, force_reg, &val);
> + if (!rc)
> + rc = sit9531x_write_u8(sitdev, force_reg, val | BIT(pair));
> +
> + ret = sit9531x_read_u8(sitdev, state_reg, &val);
> + if (!ret)
> + ret = sit9531x_write_u8(sitdev, state_reg, val & ~BIT(pair));
> + if (ret && !rc)
> + rc = ret;
> +
> + if (rc)
> + return rc;
> +
> + ref->enabled = false;
[Severity: Low]
Suppose the force write succeeds but the state read or write fails. Can
the receiver then be disabled while ref->enabled still reads true?
After an earlier disable/enable cycle the state bit is already clear. So
after this partial failure the device holds force=1/state=0.
A SELECTABLE request that arrives before the next poll then runs this
code in sit9531x_dpll_input_pin_state_on_dpll_set():
if (!ref->enabled) {
rc = sit9531x_input_enable(sitdev, dpin->id);
...
}
It skips the enable, adds the source to the table or finds it there, and
returns success while the receiver stays off.
The poll refreshes ref->enabled later, but nothing re-applies the enable.
[ ... ]
> @@ -390,6 +573,107 @@ static void sit9531x_prio_mask_build(struct sit9531x_dev *sitdev, u8 pll_idx,
[ ... ]
> +static int sit9531x_prio_table_commit(struct sit9531x_dev *sitdev, u8 pll_idx,
> + const u8 *srcs)
> +{
> + u8 val, slot, attempt, written = 0;
> + int rc = 0, prg_rc, ho_rc = 0;
> + u16 reg;
> +
> + rc = sit9531x_update_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_HO_CTRL,
> + BIT(SIT9531X_PLL_HO_FORCE_BIT),
> + BIT(SIT9531X_PLL_HO_FORCE_BIT));
> + if (rc)
> + return rc;
> +
> + usleep_range(10000, 12000);
[Severity: Medium]
Does the holdover force bit take effect without a PLL-page small-change
update?
Elsewhere in the driver, PLL-page control writes are followed by
SIT9531X_PLL_REG_SMALL_UPDATE. For example, sit9531x_dpll_mode_set()
does:
rc = sit9531x_write_pll_u8(sitdev, sitdpll->id,
SIT9531X_PLL_REG_SMALL_UPDATE,
SIT9531X_SMALL_UPDATE_CMD);
sit9531x_output_phase_flush() latches PHFL_CTRL the same way. regs.h also
says that every page carries its own copy of the directive register.
If HO_CTRL follows the same rule, the PLL would never enter forced
holdover while the table is inconsistent.
The release further down can also fail (the ho_rc path). In that case,
could force=1 stay staged and then be latched by an unrelated PLL-page
update, such as mode_set() or a phase flush?
Nothing reads HO_FREEZE to confirm that holdover was entered, so the
10-12 ms sleep is also an assumption. All of this depends on the HO_CTRL
latch behaviour in the datasheet, which I couldn't confirm here.
> +
> + /*
> + * 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 commit;
[Severity: Medium]
If one of these writes fails partway through, the goto commit path still
latches through sit9531x_prio_prg_commit() and releases holdover. Does
that leave the PLL selecting from a table that mixes new and old register
pairs, with the original never restored?
For example, once prio_set is wired up, moving source 3 to slot 0 of
[0,1,2,3,...] builds [3,0,1,2,...]. If the second register write fails,
the device keeps [3,0,2,3,...] and source 1 is no longer in the table.
At this commit, sit9531x_prio_mask_build() also returns early when
written < SIT9531X_PRIO_MAX_SLOTS. chan->prio_mask therefore still
describes the table from before the request.
The rollback checks in sit9531x_dpll_input_pin_state_on_dpll_set() go
through sit9531x_input_prio_present(). With a stale mask, could they
make the wrong decision?
On SELECTABLE, the setter could turn off a receiver it has just enabled,
for an input that the latched table now lists. On DISCONNECTED, it could
keep the claim and leave a receiver powered with no owner.
The next commit, "dpll: sit9531x: add support to get and set priority on
input pins", refreshes the mask per slot, which fixes the stale-mask
rollback. However, its restore block sits after
written = SIT9531X_PRIO_MAX_SLOTS, where rc is always 0, and every failure
path jumps past it with goto commit. The mixed table is still latched at
the end of the series.
There is also a separate case: every slot is written but the
SMALL_UPDATE latch write fails. Then written == SIT9531X_PRIO_MAX_SLOTS,
and the requested mask is published even though the device may still be
selecting from the old table.
[ ... ]
> +commit:
> + /*
> + * Latch unconditionally: slots written before a failed write 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 from the table just written, so a get that
> + * follows a set does not have to wait for the next poll -- but only
> + * for the slots that reached the device. Describing a table that
> + * does not exist would make the membership test answer for writes
> + * that failed, and that test is what decides whether a failed
> + * request gets rolled back.
> + */
> + sit9531x_prio_mask_build(sitdev, pll_idx, srcs, written);
[Severity: Medium]
The v10 notes say:
Priority table coherence: a table that was written only in part is put
back rather than latched; the mask describes what reached the device
rather than what was asked for;
Does the code match that? Every failure above jumps here and latches
unconditionally. Nothing saves the original table or writes it back.
This comment promises a refresh "only for the slots that reached the
device". At this commit, however, sit9531x_prio_mask_build() does:
if (written < SIT9531X_PRIO_MAX_SLOTS)
return;
So after a partial write, the mask keeps describing the table from before
the request, not the table that was latched.
The next commit in the series refreshes the mask per slot and adds an
if (rc && written) restore block. That block is placed after
written = SIT9531X_PRIO_MAX_SLOTS, where rc is 0, and every failure path
jumps past it.
Could the notes and comments be changed to match the code, or the restore
be made reachable?
[ ... ]
> @@ -414,6 +698,242 @@ static int sit9531x_prio_table_read(struct sit9531x_dev *sitdev, u8 pll_idx,
[ ... ]
> +/*
> + * sit9531x_input_prio_set - move an input to a priority slot
[ ... ]
> + * Caller must hold sitdev->multiop_lock.
> + *
> + * Return: 0 on success, -EINVAL if the source is not in the table,
> + * <0 on error
> + */
[Severity: Low]
This isn't a bug, but the Return: line doesn't list -ERANGE. The function
returns it here:
if (prio > n)
return -ERANGE;
The prio_set callback added later in the series handles -ERANGE as its
own case.
-EINVAL is also returned for a bad pll_idx or prio, not only when the
source is missing from the table.
[ ... ]
> @@ -893,6 +1414,41 @@ static void sit9531x_dev_ref_states_update(struct sit9531x_dev *sitdev)
[ ... ]
> +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);
[Severity: Medium]
What happens here for a differentially configured pair whose table entry
uses the N-lane code (7-10)?
sit9531x_prio_src_canon() and sit9531x_ref_pll_mask_fetch() both fold N
onto P. At this commit, though, sit9531x_prio_mask_build() sets BIT(src)
on the raw code, and this rebuild maps that code through
sit9531x_hw_src_input() to the odd N-lane index.
The claim then lands on ref[N] instead of ref[P]. Could a disconnect on
another DPLL then find ref[P].pll_mask at zero and power down a shared
receiver that this PLL is still using?
The same raw mask makes the P pin report DISCONNECTED. A SELECTABLE
request then does nothing, because sit9531x_input_prio_add() finds the
canonicalised entry and returns 0.
The next commit, "dpll: sit9531x: add support to get and set priority on
input pins", builds prio_mask from canonical codes. That fixes the mask,
membership and refcount parts.
The selected_ref part is still unfolded at the end of the series.
sit9531x_chan_state_fetch() stores:
chan->selected_ref =
sit9531x_hw_src_input(input_sel & SIT9531X_PRIO_NIBBLE_MASK);
sit9531x_dpll_selection_state_get() compares that value with the P-lane
pin id. A DE pair selected through its N code can therefore never report
CONNECTED. I couldn't confirm from the datasheet whether ACTIVESEL reports
N codes for a DE pair.
[ ... ]
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index 63c72bba51bfe..4437c95f578f8 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -286,6 +286,45 @@ 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.
> + *
> + * CONNECTED means the device has selected this pin, not that the loop
> + * has settled on it: how well it is tracking is what lock status answers,
> + * and a PLL following a reference it has not locked to yet is still
> + * following that reference and no other.
[ ... ]
> +static void
> +sit9531x_dpll_selection_state_get(struct sit9531x_dev *sitdev,
> + const struct sit9531x_dpll *sitdpll,
> + u8 pin_id, enum dpll_pin_state *state)
> +{
> + const struct sit9531x_chan *chan;
> + bool active_input;
> +
> + lockdep_assert_held(&sitdev->multiop_lock);
> +
> + chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
> + active_input = !chan->mode && chan->selected_ref == pin_id;
[Severity: Medium]
The commit message says the contract "is written above the operations
rather than left to be inferred". The contract block above, which this
patch leaves unchanged, still says:
L chan->locked && !chan->inner_lol (only meaningful when A)
...
CONNECTED A && L && S && M
SELECTABLE M && !(A && L && S)
This getter never reads chan->locked or chan->inner_lol. A reference
that is selected but not locked therefore reports CONNECTED, where the
contract says SELECTABLE. Which behaviour is intended?
Later in the series, the FFO getter says "selected_ref alone is not
enough (free-run, LOL, holdover), so use the same CONNECTED criterion"
and gates on this function. It would then publish FFO for a PLL that has
lost lock.
The same block also says pll_mask "drifts, because .prio_set writes the
table without touching it". This patch, however, adds
sit9531x_ref_pll_mask_rebuild(), which re-derives pll_mask on every poll,
and no .prio_set exists at this commit. Both texts are unchanged at the
end of the series.
[Severity: Medium]
Separately, should CONNECTED also take chan->active and chan->ho_freeze
into account?
sit9531x_chan_state_fetch() fetches both. It also notes that on a PLL
the configuration leaves unused, the status bits stay at their defaults,
and it gates locked on active for that reason.
A default ACTIVESEL of 0 decodes to IN0P. Could IN0 then report
CONNECTED on an inactive PLL?
During holdover, the pin would also report CONNECTED while the loop is
not tracking any input. That includes the forced holdover that
sit9531x_prio_table_commit() applies on every table write.
I couldn't confirm from the datasheet what ACTIVESEL reads on an inactive
PLL.
> +
> + if (!(chan->prio_mask & BIT(sit9531x_input_hw_src(pin_id))))
> + *state = DPLL_PIN_STATE_DISCONNECTED;
> + else if (active_input)
> + *state = DPLL_PIN_STATE_CONNECTED;
> + else
> + *state = DPLL_PIN_STATE_SELECTABLE;
> +}
[Severity: Medium]
Does reporting hardware selection through DPLL_A_PIN_STATE match the
documented semantics? Documentation/driver-api/dpll.rst says:
Pin state (DPLL_A_PIN_STATE) reflects the administrative intent set
by the user.
It also says that pin operational state (DPLL_A_PIN_OPERSTATE) reflects
what the hardware is actually doing with the pin.
The setter refuses CONNECTED. Yet whenever selected_ref == pin_id, this
getter reports a pin the user set to SELECTABLE as CONNECTED, without
any user action.
sit9531x_dpll_input_pin_ops also has no .operstate_on_dpll_get.
include/linux/dpll.h provides that callback, and zl3073x implements it to
report ACTIVE/STANDBY.
Would it fit the uAPI better to keep state administrative and report the
selection through operstate?
[ ... ]
> @@ -414,6 +698,242 @@ static int sit9531x_prio_table_read(struct sit9531x_dev *sitdev, u8 pll_idx,
> return 0;
> }
>
> +/*
> + * sit9531x_input_prio_set - move an input to a priority slot
> + * @input_idx: input source in hardware encoding (0-11, see
> + * sit9531x_input_hw_src())
> + * @prio: priority slot position (0 = highest)
> + *
> + * Reads the PLL's table, takes the source out of wherever it sits and
> + * reinserts it at @prio, shifting the entries in between. The rest keep
> + * their relative order: a priority change asks about one input, so the
> + * fallbacks configured behind it have to survive it.
> + *
> + * The table is what makes a source eligible for this PLL, so this only
> + * ever reorders sources already in it. A source that is absent is
> + * disconnected on this PLL, and inserting it here would make it a
> + * selection candidate again behind the caller's back; that is a connect,
> + * and it belongs to the pin's state setter.
> + *
> + * Caller must hold sitdev->multiop_lock.
> + *
> + * Return: 0 on success, -EINVAL if the source is not in the table,
> + * <0 on error
> + */
> +int sit9531x_input_prio_set(struct sit9531x_dev *sitdev, u8 pll_idx,
> + u8 input_idx, u8 prio)
> +{
> + u8 orig_srcs[SIT9531X_PRIO_MAX_SLOTS];
> + u8 rest[SIT9531X_PRIO_MAX_SLOTS];
> + u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
> + u8 slot, n = 0;
> + int rc;
> +
> + lockdep_assert_held(&sitdev->multiop_lock);
> +
> + if (pll_idx >= SIT9531X_NUM_PLLS)
> + return -EINVAL;
> + input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
> + if (input_idx >= SIT9531X_PRIO_NUM_SRC)
> + return -EINVAL;
> + if (prio >= SIT9531X_PRIO_MAX_SLOTS)
> + return -EINVAL;
> +
> + rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
> + if (rc)
> + return rc;
> +
> + memcpy(orig_srcs, srcs, sizeof(srcs));
> +
> + /*
> + * Take every copy of the source out first. A disconnect backfills
> + * the slots it frees with the lowest-priority source still listed,
> + * so the same source appearing more than once is an ordinary state
> + * of the table, and shifting from its first copy alone can leave
> + * another copy ahead of the slot the request named -- a priority
> + * the request did not ask for and the getter would then report.
> + */
> + for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++)
> + if (sit9531x_prio_src_canon(sitdev, srcs[slot]) != input_idx)
> + rest[n++] = srcs[slot];
> +
> + if (n == SIT9531X_PRIO_MAX_SLOTS)
> + return -EINVAL;
> +
> + /*
> + * Once the copies are collapsed the source can only sit behind the
> + * other sources the table lists, because the slots past the last of
> + * them repeat it. A slot beyond that is a priority the table cannot
> + * express, and placing the source at the nearest one it can would
> + * report success for a priority nobody asked for.
> + */
> + if (prio > n)
> + return -ERANGE;
> +
> + for (slot = 0; slot < prio; slot++)
> + srcs[slot] = rest[slot];
> +
> + srcs[prio] = input_idx;
> +
> + for (slot = prio + 1; slot < SIT9531X_PRIO_MAX_SLOTS; slot++)
> + srcs[slot] = (slot - 1 < n) ? rest[slot - 1] : srcs[slot - 1];
> + if (!memcmp(srcs, orig_srcs, sizeof(srcs)))
> + return 0;
> +
> + return sit9531x_prio_table_commit(sitdev, pll_idx, srcs);
> +}
> +
> +/*
> + * sit9531x_input_prio_remove - drop an input from a PLL's priority table
> + * @input_idx: input source in hardware encoding
> + *
> + * Rewrites the priority table with the source removed: the remaining
> + * sources are compacted toward the highest-priority slots and the freed
> + * tail slots are backfilled with the lowest-priority remaining source.
> + * This makes a disconnected input ineligible for automatic reference
> + * selection, not just gated at the input buffer.
> + *
> + * Removing a source that is absent is what the caller asked for already,
> + * so it succeeds without touching the table. Removing the last one fills
> + * every slot with the code for the pair this part does not have, which
> + * leaves the device with nothing to select and no reference to fall back
> + * on -- which is what a request to disconnect the last input means.
> + *
> + * Caller must hold sitdev->multiop_lock.
> + *
> + * Return: 0 on success, <0 on error
> + */
> +int sit9531x_input_prio_remove(struct sit9531x_dev *sitdev, u8 pll_idx,
> + u8 input_idx)
> +{
> + u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
> + u8 kept[SIT9531X_PRIO_MAX_SLOTS];
> + u8 slot, count = 0;
> + bool found = false;
> + int rc;
> +
> + lockdep_assert_held(&sitdev->multiop_lock);
> +
> + if (pll_idx >= SIT9531X_NUM_PLLS)
> + return -EINVAL;
> + input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
> +
> + rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
> + if (rc)
> + return rc;
> +
> + for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
> + if (sit9531x_prio_src_canon(sitdev, srcs[slot]) == input_idx)
> + found = true;
> + else
> + kept[count++] = srcs[slot];
> + }
> +
> + if (!found)
> + return 0;
> +
> + if (count == 0) {
> + /*
> + * Nothing is left to compact toward, so every slot gets the
> + * code that names no reference. The device is then left
> + * with no source to select, which is what disconnecting the
> + * last input asks for; lock status follows the loop on its
> + * own from there.
> + */
> + memset(kept, SIT9531X_PRIO_SRC_NONE, sizeof(kept));
> + } else {
> + /* Backfill freed tail slots with the lowest-priority src */
> + while (count < SIT9531X_PRIO_MAX_SLOTS) {
> + kept[count] = kept[count - 1];
> + count++;
> + }
> + }
> +
> + return sit9531x_prio_table_commit(sitdev, pll_idx, kept);
> +}
> +
> +/*
> + * sit9531x_input_prio_add - make an input eligible in a PLL's table
> + * @input_idx: input source in hardware encoding
> + *
> + * Ensures the source appears in the priority table so it can be picked
> + * by automatic reference selection again after a disconnect. If the
> + * source is already listed the table is left untouched; otherwise it
> + * replaces a duplicate at the tail. A table that contains only unique
> + * sources cannot accept a new one without evicting another, so that
> + * case fails with -ENOSPC.
> + *
> + * Caller must hold sitdev->multiop_lock.
> + */
> +int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
> + u8 input_idx)
> +{
> + u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
> + u8 seen[SIT9531X_PRIO_NUM_SRC] = { 0 };
> + u8 replace = SIT9531X_PRIO_MAX_SLOTS;
> + u8 slot, src, src_canon;
> + int rc;
> +
> + lockdep_assert_held(&sitdev->multiop_lock);
> +
> + if (pll_idx >= SIT9531X_NUM_PLLS)
> + return -EINVAL;
> + input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
> + if (input_idx >= SIT9531X_PRIO_NUM_SRC)
> + return -EINVAL;
> +
> + rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
> + if (rc)
> + return rc;
> +
> + for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++)
> + if (sit9531x_prio_src_canon(sitdev, srcs[slot]) == input_idx)
> + return 0;
> +
> + for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
> + src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
> + src_canon = sit9531x_prio_src_canon(sitdev, src);
> + if (!sit9531x_prio_src_usable(src))
> + continue;
> +
> + seen[src_canon]++;
> + }
> +
> + /*
> + * A slot whose code names no usable source -- a reserved value, or
> + * one past the inputs this variant has -- is free space, and taking
> + * it costs nothing. Prefer it over displacing a real reference.
> + */
> + for (slot = SIT9531X_PRIO_MAX_SLOTS; slot-- > 0;) {
> + src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
> + if (!sit9531x_prio_src_usable(src)) {
> + replace = slot;
> + break;
> + }
> + }
> +
> + /* Otherwise take the last slot holding a source listed twice. */
> + for (slot = SIT9531X_PRIO_MAX_SLOTS;
> + replace == SIT9531X_PRIO_MAX_SLOTS && slot-- > 0;) {
> + src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
> + src_canon = sit9531x_prio_src_canon(sitdev, src);
> + if (!sit9531x_prio_src_usable(src))
> + continue;
> +
> + if (seen[src_canon] > 1) {
> + replace = slot;
> + break;
> + }
> + }
> +
> + if (replace == SIT9531X_PRIO_MAX_SLOTS)
> + return -ENOSPC;
> +
> + srcs[replace] = input_idx;
> +
> + return sit9531x_prio_table_commit(sitdev, pll_idx, srcs);
> +}
> +
> /* XO doubler register */
> #define SIT9531X_REG_XO2_GENERIC SIT9531X_REG(0x00, 0x2D)
> #define SIT9531X_XO_DOUBLER_ENB_BIT 7 /* inverted: 0 = enabled */
[Severity: Low]
When the table has more than one free slot, does this search place new
inputs in the reverse of the order they were connected?
The loop walks down from the last slot, so it always takes the
highest-index free slot. After sit9531x_input_prio_remove() has emptied
the table, every slot holds SIT9531X_PRIO_SRC_NONE. Suppose IN0 and
then IN2 are set to SELECTABLE on that DPLL:
- IN0 lands in the last slot.
- IN2 lands in the slot before it, so IN2 now outranks IN0.
Each input connected afterwards goes ahead of every input connected
before it, until the free slots run out. The same happens when the
loaded configuration leaves several slots holding codes that
sit9531x_prio_src_usable() rejects.
The SELECTABLE path in sit9531x_dpll_input_pin_state_on_dpll_set()
does not report where the input landed. The user only sees the order
the PLL will select in by reading the priorities back. The priority
setter added in the next patch can reorder them, but the default order
is still the opposite of what one would expect.
Was this ordering intended? Two alternatives:
- Scan upward from slot 0 for free space.
- Place the new source directly behind the last usable entry.
Either would keep the order in which inputs were connected. The
kernel-doc above only says the source replaces a duplicate at the
tail. It does not describe the free-slot case, or which free slot is
preferred.
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH v10 07/14] dpll: sit9531x: add support to get and set priority on input pins
2026-09-21 20:11 [PATCH v10 00/14] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (5 preceding siblings ...)
2026-09-21 20:11 ` [PATCH v10 06/14] dpll: sit9531x: implement input pin state on a DPLL Ali Rouhi
@ 2026-09-21 20:11 ` Ali Rouhi
2026-09-26 2:34 ` Jakub Kicinski
2026-09-21 20:11 ` [PATCH v10 08/14] dpll: sit9531x: add support to get and set frequency on pins Ali Rouhi
` (6 subsequent siblings)
13 siblings, 1 reply; 28+ messages in thread
From: Ali Rouhi @ 2026-09-21 20:11 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi,
prabhakar.mahadev-lad.rj, dev, devicetree, netdev, linux-kernel,
Ali Rouhi
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
The priority table is ordered: the slot an input occupies is its
priority, and the device selects the lowest occupied slot whose signal is
qualified. Getting the priority is therefore a search of the table for
the input, and setting it moves the input to the requested slot.
A priority change can shift the slots of the other inputs on the same
PLL. The core notifies only the pin the request named, so this callback
notifies the rest itself, with __dpll_pin_change_ntf(): the core already
holds the device lock across a pin op, and that helper is the one that
expects to be called with it held.
The two directions treat an absent input differently, because the core
asks different things of them. A set names a slot to move an input to,
so an input that is not in the table is refused with -EINVAL rather than
inserted behind the request's back. A get has to return a number, so an
absent input reports the lowest slot -- the same value a real
lowest-priority input reports, since the interface has no way to say
"not in the table".
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
Notes:
Changes in v10:
Notified the sibling pins with the helper meant for a caller that
already holds the device lock, left the pin the request named to the
core, and skipped a pin that is being unregistered.
drivers/dpll/sit9531x/core.c | 119 ++++++++++++++++++++++++++------
drivers/dpll/sit9531x/core.h | 10 +++
drivers/dpll/sit9531x/dpll.c | 129 +++++++++++++++++++++++++++++++++++
drivers/dpll/sit9531x/prop.c | 2 +
4 files changed, 240 insertions(+), 20 deletions(-)
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 84c1f86e6d4c..ac184c93258c 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -543,34 +543,87 @@ bool sit9531x_input_prio_present(struct sit9531x_dev *sitdev, u8 pll_idx,
}
/*
- * Rebuild a PLL's membership mask from the source codes of its priority
- * table. The mask is what the pin state getters test, so it is refreshed
- * from exactly the values the table holds -- here after a write, and once
- * per poll from the read-back in sit9531x_chan_state_fetch().
+ * sit9531x_input_prio_get - read an input's priority slot for a PLL
+ * @input_idx: input source in hardware encoding (see
+ * sit9531x_input_hw_src())
+ * @prio: output slot position (0 = highest)
+ *
+ * Reports the last slot this source occupied on this PLL. The value is
+ * cached from the hardware table read at startup and refreshed after every
+ * table write and poll read-back, so pin-get reflects hardware state without
+ * issuing synchronous register reads per pin. A source with no known slot
+ * falls back to the lowest-priority valid slot.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_input_prio_get(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 input_idx, u8 *prio)
+{
+ const struct sit9531x_chan *chan;
+ u8 slot;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+ input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+ if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+ return -EINVAL;
+
+ chan = &sitdev->chan[pll_idx];
+ slot = chan->prio_last[input_idx];
+ if (!slot)
+ slot = SIT9531X_PRIO_MAX_SLOTS;
+
+ *prio = slot - 1;
+
+ return 0;
+}
+
+/*
+ * Refresh a PLL's cached view of its priority table from the source codes
+ * the table holds -- here after a write, and once per poll from the
+ * read-back in sit9531x_chan_state_fetch().
+ *
+ * The membership mask is what the pin state getters test; the per-slot
+ * copy and the last-slot-seen array are what priority get answers from,
+ * so neither costs a register read per pin.
*/
static void sit9531x_prio_mask_build(struct sit9531x_dev *sitdev, u8 pll_idx,
const u8 *srcs, u8 written)
{
+ struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+ u8 first[SIT9531X_PRIO_NUM_SRC] = { 0 };
u16 mask = 0;
- u8 slot;
-
- /*
- * A table written only in part is not described by the values the
- * request carried, and there is nothing here to describe it with
- * instead, so the mask it had stands until the next poll reads the
- * table back.
- */
- if (written < SIT9531X_PRIO_MAX_SLOTS)
- return;
+ u8 slot, src, src_canon;
for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
- u8 src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
+ /*
+ * A slot the caller could not write still holds what it
+ * held before, so take that rather than the value the
+ * request wanted to put there.
+ */
+ src = slot < written ? srcs[slot] : chan->prio_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;
- if (sit9531x_prio_src_usable(src))
- mask |= BIT(src);
+ mask |= BIT(src_canon);
+ if (!first[src_canon])
+ first[src_canon] = slot + 1;
}
- sitdev->chan[pll_idx].prio_mask = mask;
+ /*
+ * 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. */
@@ -579,7 +632,8 @@ static void sit9531x_prio_mask_build(struct sit9531x_dev *sitdev, u8 pll_idx,
static int sit9531x_prio_table_commit(struct sit9531x_dev *sitdev, u8 pll_idx,
const u8 *srcs)
{
- u8 val, slot, attempt, written = 0;
+ struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+ u8 val, slot, attempt, written = 0, restored = 0;
int rc = 0, prg_rc, ho_rc = 0;
u16 reg;
@@ -629,9 +683,34 @@ static int sit9531x_prio_table_commit(struct sit9531x_dev *sitdev, u8 pll_idx,
written = SIT9531X_PRIO_MAX_SLOTS;
+ 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;
+ }
+
commit:
/*
- * Latch unconditionally: slots written before a failed write are in
+ * 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.
*/
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 033e1059bde9..2c5d0100b450 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -126,6 +126,12 @@ struct sit9531x_out {
* @ho_freeze: holdover freeze active
* @ho_valid: holdover memory acquired, i.e. the holdover window
* holds a valid estimate to fall back on
+ * @prio_srcs: cached copy of the priority table, one source code
+ * per slot; refreshed together with @prio_mask, so
+ * priority reads generate no register traffic
+ * @prio_last: slot each source occupies, plus one (0 = the source
+ * is not in the table); refreshed from the same scan
+ * as @prio_mask, so the two never disagree
* @prio_mask: bit per hardware source code present in this PLL's
* priority table, i.e. the sources it may select. Read
* back from the table by the periodic worker and
@@ -140,6 +146,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;
};
@@ -237,6 +245,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 4437c95f578f..56a8213dee07 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -489,10 +489,139 @@ sit9531x_dpll_input_pin_state_on_dpll_set(const struct dpll_pin *pin,
return rc;
}
+/*
+ * sit9531x_dpll_input_pin_prio_get - read input pin priority
+ *
+ * Reports the cached slot from sit9531x_input_prio_get(). The cache is
+ * refreshed from hardware at startup and by periodic read-back, so pin-get
+ * reports hardware priority without synchronous per-pin I2C reads.
+ */
+static int
+sit9531x_dpll_input_pin_prio_get(const struct dpll_pin *pin, void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, u32 *prio,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ u8 slot;
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_input_prio_get(sitdev, sitdpll->id,
+ sit9531x_input_hw_src(dpin->id), &slot);
+ mutex_unlock(&sitdev->multiop_lock);
+ if (rc)
+ return rc;
+
+ dpin->prio = slot;
+ *prio = slot;
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_input_pin_prio_set - set input pin priority
+ *
+ * writes input priority table on Page 1 via
+ * core.c sit9531x_input_prio_set(). Forces holdover during update.
+ */
+static int
+sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, u32 prio,
+ struct netlink_ext_ack *extack)
+{
+ struct dpll_pin *changed[SIT9531X_MAX_INPUTS + 1];
+ struct sit9531x_dpll_pin *sibling;
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ u8 changed_cnt = 0, hw_src, slot;
+ int get_rc, rc;
+
+ if (dpin->dir != DPLL_PIN_DIRECTION_INPUT) {
+ NL_SET_ERR_MSG(extack, "Priority applies only to input pins");
+ return -EINVAL;
+ }
+
+ if (prio >= SIT9531X_PRIO_MAX_SLOTS) {
+ NL_SET_ERR_MSG(extack, "Priority out of range (0-10)");
+ return -EINVAL;
+ }
+
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_input_prio_set(sitdev, sitdpll->id,
+ sit9531x_input_hw_src(dpin->id),
+ (u8)prio);
+ if (!rc) {
+ list_for_each_entry(sibling, &sitdpll->pins, list) {
+ if (!sit9531x_dpll_is_input_pin(sibling) ||
+ sit9531x_dpll_is_xo_pin(sibling))
+ continue;
+
+ hw_src = sit9531x_input_hw_src(sibling->id);
+ get_rc = sit9531x_input_prio_get(sitdev, sitdpll->id,
+ hw_src, &slot);
+ if (get_rc)
+ continue;
+
+ if (sibling->prio == slot)
+ continue;
+
+ sibling->prio = slot;
+
+ /*
+ * The core notifies the pin the request named, so
+ * only the others are collected here. A pin whose
+ * dpll_pin is already NULL is mid-unregister: that
+ * runs with the device lock dropped between the
+ * unregister and the free, so it can be seen from
+ * here, and notifying through it would follow a
+ * pointer that is on its way out.
+ */
+ if (sibling == dpin || !sibling->dpll_pin)
+ continue;
+
+ if (changed_cnt < ARRAY_SIZE(changed))
+ changed[changed_cnt++] = sibling->dpll_pin;
+ }
+ }
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc == -EINVAL) {
+ NL_SET_ERR_MSG(extack,
+ "Pin is not a reference of this DPLL; connect it first");
+ return rc;
+ }
+ if (rc == -ERANGE) {
+ NL_SET_ERR_MSG(extack,
+ "Priority is past the last reference this DPLL lists");
+ return rc;
+ }
+ if (rc) {
+ NL_SET_ERR_MSG(extack, "Failed to set input priority");
+ return rc;
+ }
+
+ /*
+ * The core notifies only the pin the request named, so the ones whose
+ * slots moved are notified here. This runs inside a pin op, where
+ * the core already holds the lock the notification needs, so it is
+ * the underscore helper rather than the wrapper that takes it.
+ */
+ while (changed_cnt--)
+ __dpll_pin_change_ntf(changed[changed_cnt]);
+
+ return 0;
+}
+
static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
.direction_get = sit9531x_dpll_input_pin_direction_get,
.state_on_dpll_get = sit9531x_dpll_input_pin_state_on_dpll_get,
.state_on_dpll_set = sit9531x_dpll_input_pin_state_on_dpll_set,
+ .prio_get = sit9531x_dpll_input_pin_prio_get,
+ .prio_set = sit9531x_dpll_input_pin_prio_set,
};
/*
diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
index 1a09bd168163..4c6a2249300f 100644
--- a/drivers/dpll/sit9531x/prop.c
+++ b/drivers/dpll/sit9531x/prop.c
@@ -191,6 +191,7 @@ sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
*/
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] 28+ messages in thread* Re: [PATCH v10 07/14] dpll: sit9531x: add support to get and set priority on input pins
2026-09-21 20:11 ` [PATCH v10 07/14] dpll: sit9531x: add support to get and set priority on input pins Ali Rouhi
@ 2026-09-26 2:34 ` Jakub Kicinski
0 siblings, 0 replies; 28+ messages in thread
From: Jakub Kicinski @ 2026-09-26 2:34 UTC (permalink / raw)
To: arouhi
Cc: Jakub Kicinski, jiri, vadim.fedorenko, arkadiusz.kubalewski,
ivecera, robh, krzk+dt, conor+dt, cjubran, pabeni,
Oleg.Zadorozhnyi, prabhakar.mahadev-lad.rj, dev, devicetree,
netdev, linux-kernel
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
dpll: sit9531x: add support to get and set priority on input pins
This adds prio_get and prio_set callbacks for the sit9531x input pins.
They are backed by a cached copy of each PLL's priority table, which
sit9531x_prio_mask_build() now maintains. prio_set moves an input to the
requested slot and notifies the other pins whose slots shifted, and
sit9531x_prio_table_commit() gains a rollback of partially written slots.
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index 84c1f86e6d4cf..ac184c93258cb 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -543,34 +543,87 @@ bool sit9531x_input_prio_present(struct sit9531x_dev *sitdev, u8 pll_idx,
> }
>
> /*
> - * Rebuild a PLL's membership mask from the source codes of its priority
> - * table. The mask is what the pin state getters test, so it is refreshed
> - * from exactly the values the table holds -- here after a write, and once
> - * per poll from the read-back in sit9531x_chan_state_fetch().
> + * sit9531x_input_prio_get - read an input's priority slot for a PLL
> + * @input_idx: input source in hardware encoding (see
> + * sit9531x_input_hw_src())
> + * @prio: output slot position (0 = highest)
> + *
> + * Reports the last slot this source occupied on this PLL. The value is
[Severity: Low]
This isn't a bug, but is "last slot" accurate here?
sit9531x_prio_mask_build() fills prio_last from first[], which holds the
first (highest priority) slot a source occupies among its duplicates. It
also clears the entry to 0 as soon as the source leaves the table:
if (!first[src_canon])
first[src_canon] = slot + 1;
The "last-slot-seen array" wording in the sit9531x_prio_mask_build()
comment below has the same problem. The @prio_last description in core.h
does match the code.
[ ... ]
> +/*
> + * Refresh a PLL's cached view of its priority table from the source codes
> + * the table holds -- here after a write, and once per poll from the
> + * read-back in sit9531x_chan_state_fetch().
> + *
> + * The membership mask is what the pin state getters test; the per-slot
> + * copy and the last-slot-seen array are what priority get answers from,
> + * so neither costs a register read per pin.
> */
> static void sit9531x_prio_mask_build(struct sit9531x_dev *sitdev, u8 pll_idx,
> const u8 *srcs, u8 written)
> {
> + struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
> + u8 first[SIT9531X_PRIO_NUM_SRC] = { 0 };
> u16 mask = 0;
> - u8 slot;
> -
> - /*
> - * A table written only in part is not described by the values the
> - * request carried, and there is nothing here to describe it with
> - * instead, so the mask it had stands until the next poll reads the
> - * table back.
> - */
> - if (written < SIT9531X_PRIO_MAX_SLOTS)
> - return;
> + u8 slot, src, src_canon;
>
> for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
> - u8 src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
> + /*
> + * A slot the caller could not write still holds what it
> + * held before, so take that rather than the value the
> + * request wanted to put there.
> + */
> + src = slot < written ? srcs[slot] : chan->prio_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;
>
> - if (sit9531x_prio_src_usable(src))
> - mask |= BIT(src);
> + mask |= BIT(src_canon);
> + if (!first[src_canon])
> + first[src_canon] = slot + 1;
> }
[Severity: Low]
This isn't a bug, but this hunk changes sit9531x_prio_mask_build() in two
ways that the commit message doesn't mention. Both affect existing users:
the pin state getter, sit9531x_input_prio_present(),
sit9531x_ref_pll_mask_rebuild() and the periodic poll.
First, the mask is now built from BIT(src_canon) instead of BIT(src).
Before this patch, when a differential pair's table entry held the N-lane
code, the P-lane pin was reported as disconnected and prio_present()
returned false. Now the same entry reports the pin as listed.
Second, the early return on a partial write is gone. The requested values
for the written slots are now merged with chan->prio_srcs for the rest.
That changes what prio_present() tells the rollback branches of
sit9531x_dpll_input_pin_state_on_dpll_set() after a failed commit.
Should these changes be described in the commit message, or moved into
the commit that introduced sit9531x_prio_mask_build()?
[ ... ]
> @@ -629,9 +683,34 @@ static int sit9531x_prio_table_commit(struct sit9531x_dev *sitdev, u8 pll_idx,
>
> written = SIT9531X_PRIO_MAX_SLOTS;
>
> + 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;
> + }
[Severity: Medium]
Can this block ever run? Every failing register access earlier in
sit9531x_prio_table_commit() jumps straight to the label:
rc = sit9531x_write_u8(sitdev, reg, val);
if (rc)
goto commit;
The read and the write of the last register use the same goto. The only
way to reach "if (rc && written)" is to fall through after the last write
succeeded, and rc is 0 at that point.
So when a table write fails partway through prio_set, prio_add or
prio_remove, sit9531x_prio_prg_commit() still latches the half-written
table and the forced holdover is still released.
For example, take [A,B,C,D,E,F,G,H,X,I,J] and move X to slot 0. If the
write for slots 4 and 5 fails, the device latches
[X,A,B,C,E,F,G,H,X,I,J], and D drops out of this PLL's selection list.
The partial latch itself comes from the earlier "dpll: sit9531x: implement
input pin state on a DPLL" commit. The comment here, though, says this
block prevents it.
If the gotos were pointed at this block, would a few more things need
fixing?
With written == SIT9531X_PRIO_MAX_SLOTS (11), the loop reaches slot 10 and
reads chan->prio_srcs[11], one past the end of the 11-entry array.
The last register shares its other nibble with CLK_ACTIVESEL. This block
rewrites that register from a 0 base, not with the read-modify-write the
forward path uses.
After "written = restored", sit9531x_prio_mask_build(sitdev, pll_idx,
srcs, written) would cache the requested srcs[] for exactly the slots that
were just put back to their old values.
The commit message also doesn't mention this change to a helper shared by
prio_add, prio_remove and prio_set.
[ ... ]
> diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
> index 033e1059bde9b..2c5d0100b450e 100644
> --- a/drivers/dpll/sit9531x/core.h
> +++ b/drivers/dpll/sit9531x/core.h
> @@ -126,6 +126,12 @@ struct sit9531x_out {
> * @ho_freeze: holdover freeze active
> * @ho_valid: holdover memory acquired, i.e. the holdover window
> * holds a valid estimate to fall back on
> + * @prio_srcs: cached copy of the priority table, one source code
> + * per slot; refreshed together with @prio_mask, so
> + * priority reads generate no register traffic
> + * @prio_last: slot each source occupies, plus one (0 = the source
> + * is not in the table); refreshed from the same scan
> + * as @prio_mask, so the two never disagree
> * @prio_mask: bit per hardware source code present in this PLL's
> * priority table, i.e. the sources it may select. Read
[Severity: Low]
This isn't a bug, but the @prio_mask description still says "bit per
hardware source code".
sit9531x_prio_mask_build() now sets BIT(src_canon). An N-lane code in the
table therefore sets the P-lane bit and leaves its own bit clear.
Could this be updated to say the mask is indexed by the canonical source?
[ ... ]
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index 4437c95f578f8..56a8213dee077 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -489,10 +489,139 @@ sit9531x_dpll_input_pin_state_on_dpll_set(const struct dpll_pin *pin,
[ ... ]
> +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) {
[Severity: Low]
If sit9531x_input_prio_set() returns an error after the table has already
changed, is the sibling update skipped entirely?
In sit9531x_prio_table_commit(), the table is latched and
sit9531x_prio_mask_build() refreshes prio_last before the holdover release
is retried. A failure there still becomes the return value:
if (ho_rc) {
...
if (!rc)
rc = ho_rc;
A partially written table is also latched and cached on the write failure
paths.
In both cases this callback returns an error, so dpll_pin_prio_set() in
the core also skips __dpll_pin_change_ntf() for the named pin. Several
pins' reported priorities can then change with no PIN_CHANGE_NTF for any
of them, and sibling->prio stays stale.
> + list_for_each_entry(sibling, &sitdpll->pins, list) {
> + if (!sit9531x_dpll_is_input_pin(sibling) ||
> + sit9531x_dpll_is_xo_pin(sibling))
> + continue;
> +
> + hw_src = sit9531x_input_hw_src(sibling->id);
> + get_rc = sit9531x_input_prio_get(sitdev, sitdpll->id,
> + hw_src, &slot);
> + if (get_rc)
> + continue;
> +
> + if (sibling->prio == slot)
> + continue;
> +
> + sibling->prio = slot;
> +
> + /*
> + * The core notifies the pin the request named, so
> + * only the others are collected here. A pin whose
> + * dpll_pin is already NULL is mid-unregister: that
> + * runs with the device lock dropped between the
> + * unregister and the free, so it can be seen from
> + * here, and notifying through it would follow a
> + * pointer that is on its way out.
> + */
> + if (sibling == dpin || !sibling->dpll_pin)
> + continue;
> +
> + if (changed_cnt < ARRAY_SIZE(changed))
> + changed[changed_cnt++] = sibling->dpll_pin;
> + }
> + }
> + mutex_unlock(&sitdev->multiop_lock);
[Severity: High]
Is it safe to walk sitdpll->pins and read sibling->dpll_pin here while
holding only multiop_lock and the core's dpll_lock? The teardown path
takes neither lock for the list or for this field:
drivers/dpll/sit9531x/core.c:sit9531x_dpll_pin_unregister() {
...
dpll_pin_unregister(sitdpll->dpll_dev, pin->dpll_pin, ops, pin);
dpll_pin_put(pin->dpll_pin, &pin->tracker);
pin->dpll_pin = NULL;
...
}
drivers/dpll/sit9531x/core.c:sit9531x_dpll_pins_unregister() {
...
list_for_each_entry_safe(pin, next, &sitdpll->pins, list) {
sit9531x_dpll_pin_unregister(pin);
list_del(&pin->list);
kfree(pin);
}
}
dpll_pin_unregister() and dpll_pin_put() each take and drop dpll_lock.
The NULL store only happens after both.
Suppose a PIN_SET prio request arrives for a pin that is still registered
on the same DPLL while sit9531x_dev_stop() or a probe error path is
running. Could this loop pick up a sibling's dpll_pin after the last
dpll_pin_put() has freed it, and pass it to __dpll_pin_change_ntf()?
Could the loop also be standing on a sit9531x_dpll_pin that another CPU
is list_del()'ing and kfree()'ing? It would then write sibling->prio into
freed memory or follow LIST_POISON1.
sit9531x_dpll_pins_register() has a similar problem. It does
list_add(&pin->list, &sitdpll->pins) without the lock, after earlier pins
are already reachable from netlink.
The !sibling->dpll_pin check added in v10 reads the pointer without any
lock that orders it against teardown. It doesn't seem to close the window
the comment describes.
[ ... ]
> + /*
> + * The core notifies only the pin the request named, so the ones whose
> + * slots moved are notified here. This runs inside a pin op, where
> + * the core already holds the lock the notification needs, so it is
> + * the underscore helper rather than the wrapper that takes it.
> + */
> + while (changed_cnt--)
> + __dpll_pin_change_ntf(changed[changed_cnt]);
> +
> + return 0;
> +}
[Severity: Medium]
Sibling notifications are only sent from this prio_set path. What about
sit9531x_dpll_input_pin_state_on_dpll_set() with
DPLL_PIN_STATE_DISCONNECTED?
That path calls sit9531x_input_prio_remove(). It rebuilds the table from
the sources it keeps and backfills the tail:
} else {
/* Backfill freed tail slots with the lowest-priority src */
while (count < SIT9531X_PRIO_MAX_SLOTS) {
kept[count] = kept[count - 1];
count++;
}
}
Every later source moves up a slot, and sit9531x_prio_mask_build() updates
prio_last, which is what prio_get now reports. For example, removing A
from [A,B,C,...] moves B from 1 to 0 and C from 2 to 1.
dpll_pin_state_set() in the core only notifies the named pin. The
disconnect path neither notifies the siblings nor refreshes their
sibling->prio.
sit9531x_dpll_changes_check() doesn't catch this later either, because it
only compares pin state:
if (state != pin->pin_state) {
...
pin->pin_state = state;
dpll_pin_change_ntf(pin->dpll_pin);
}
Would userspace listening for PIN_CHANGE_NTF miss the priority changes of
the other inputs after a disconnect?
[ ... ]
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH v10 08/14] dpll: sit9531x: add support to get and set frequency on pins
2026-09-21 20:11 [PATCH v10 00/14] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (6 preceding siblings ...)
2026-09-21 20:11 ` [PATCH v10 07/14] dpll: sit9531x: add support to get and set priority on input pins Ali Rouhi
@ 2026-09-21 20:11 ` Ali Rouhi
2026-09-26 2:34 ` Jakub Kicinski
2026-09-21 20:11 ` [PATCH v10 09/14] dpll: sit9531x: implement output pin state on a DPLL Ali Rouhi
` (5 subsequent siblings)
13 siblings, 1 reply; 28+ messages in thread
From: Ali Rouhi @ 2026-09-21 20:11 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi,
prabhakar.mahadev-lad.rj, dev, devicetree, netdev, linux-kernel,
Ali Rouhi
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Both directions in one patch, since they share everything that matters.
An input's frequency is what the board presents, so it is reported from
the firmware description rather than read back: the chip has no divider on
an input whose rate it merely qualifies.
An output's frequency is the VCO divided by that output's divider, so it
is computed from the divider read back from the chip and set by writing a
new one. The VCO in turn comes from the feedback divider, which is why
the crystal rate is needed at probe. A divider write only takes effect
inside the programming state, and that state has to be left with the
output loops re-locked whatever happened in between, so the exit runs even
when a write in the middle failed and the first error is the one returned.
Programming a divider costs about a hundred milliseconds under the device
lock: a dozen or so register transactions, then the settling time the part
requires after the loop-lock command, which is a property of the hardware
rather than a conservative guess. The DPLL core holds its own lock across
the whole callback, so a frequency set on this device delays netlink
traffic for every DPLL in the system for that long. Splitting the wait
out would need the ops to complete asynchronously, which the interface
does not offer; issuing the commit without waiting would let the next
request program a part that has not settled. A rate change is a
configuration action, not something a running system does per packet, so
the cost is paid where it is visible rather than hidden behind a
completion the caller cannot wait for.
The phase flush that follows a divider write realigns every output fed by
that PLL, not only the one that changed. The flush is a per-PLL function
in the device and there is no per-output equivalent, so an output whose
rate is set while its siblings are running will step their phase too.
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
Notes:
Changes in v10:
Arithmetic and overflow: a DIVN fraction whose numerator is not below
its denominator is refused rather than divided, which could fault the
kernel from an ordinary pin get; the output divider rounds to nearest
and refuses a rate it cannot produce exactly rather than running the
nearest one and reporting success.
The VCO clamp moved into the accessor, so a frequency get and a
frequency set work from the same number.
Error reporting: entering the programming state closes the loops and
the debug key when it fails; a phase flush that fails after the divider
is committed is a warning rather than a failed rate change.
The small-change directive is written whole, not read-modify-written --
it is a command register.
drivers/dpll/sit9531x/core.c | 681 +++++++++++++++++++++++++++++++++++
drivers/dpll/sit9531x/core.h | 4 +
drivers/dpll/sit9531x/dpll.c | 94 +++++
drivers/dpll/sit9531x/prop.c | 36 +-
drivers/dpll/sit9531x/regs.h | 17 +
5 files changed, 823 insertions(+), 9 deletions(-)
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index ac184c93258c..0687ad6de861 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -418,6 +418,127 @@ static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev,
return 0;
}
+/* 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. Callers reach
+ * this function through a goto so that the chip never stays in
+ * the PRG_CMD state with its loops open; returning early here
+ * would defeat that and leave the outputs unlocked until the
+ * next successful commit.
+ */
+ /*
+ * Re-lock the loops. Leaving them open is worse than any other
+ * failure this function can report, and nothing else closes them,
+ * so retry as the priority table does with its own latch.
+ */
+ for (attempt = 0; attempt < SIT9531X_LOOP_LOCK_TRIES; attempt++) {
+ rc2 = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+ SIT9531X_LOOP_LOCK);
+ if (!rc2)
+ break;
+ usleep_range(1000, 2000);
+ }
+ if (rc2)
+ dev_err(sitdev->dev,
+ "output loops left unlocked after programming: %d\n",
+ rc2);
+
+ msleep(100);
+
+ /*
+ * Put the output-system debug block back the way the device powers
+ * up. Its key register unlocks every debug register while it holds
+ * the unlock value, and each programming sequence writes that value
+ * itself, so nothing needs it left unlocked in between.
+ */
+ rc3 = sit9531x_write_u8(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
+ SIT9531X_DEBUG_LOCK_VAL);
+
+ if (rc)
+ return rc;
+
+ return rc2 ? rc2 : rc3;
+}
+
/*
* Input priority selection
*
@@ -1013,6 +1134,11 @@ int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
return sit9531x_prio_table_commit(sitdev, pll_idx, srcs);
}
+/* Per-slot DIVO base register offsets (6 slots per page) */
+static const u8 clkout_odr_divn_base[] = {
+ 0x14, 0x24, 0x34, 0x44, 0x54, 0x64
+};
+
/* XO doubler register */
#define SIT9531X_REG_XO2_GENERIC SIT9531X_REG(0x00, 0x2D)
#define SIT9531X_XO_DOUBLER_ENB_BIT 7 /* inverted: 0 = enabled */
@@ -1026,6 +1152,561 @@ 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;
+
+ /*
+ * DT board-config override: some configs (e.g. an INTSYNC PLL)
+ * run a VCO that Fref*DIVN does not reproduce. When the board
+ * supplies the measured VCO, use it verbatim.
+ */
+ if (pll_idx < SIT9531X_NUM_PLLS && sitdev->pll_fvco[pll_idx]) {
+ *fvco = sitdev->pll_fvco[pll_idx];
+ return 0;
+ }
+
+ 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;
+
+ *fvco = mul_u64_u64_div_u64(fref, (u64)divn, SIT9531X_DIVN_SCALE);
+
+ /*
+ * A DIVN of less than one whole cycle passes the check above and
+ * still truncates the product to zero. Callers divide by this, so
+ * report the unprogrammed divider it describes rather than handing
+ * back a zero denominator.
+ */
+ if (!*fvco)
+ return -ENODATA;
+
+ /*
+ * The bands bound what the VCO can physically run at, and a rate
+ * derived from registers the loaded configuration may never have
+ * programmed can fall outside them. Clamp rather than refuse:
+ * the readback is the only estimate available, and refusing would
+ * make every output unprogrammable on such a part. Clamping here
+ * rather than in the divider calculation keeps the rate a frequency
+ * get reports and the rate a frequency set divides the same one.
+ */
+ if (*fvco < fvco_min)
+ *fvco = fvco_min;
+ else if (*fvco > fvco_max)
+ *fvco = fvco_max;
+
+ return 0;
+}
+
+/*
+ * sit9531x_output_phase_flush - flush the output phase of a PLL
+ *
+ * Fires the chip's on-demand phase-flush (PHFL) so every output divider
+ * of @pll_idx restarts aligned to the PLL phase. Without it a rewritten
+ * DIVO keeps counting from an arbitrary point and the output edge lands
+ * with a persistent offset against the tracked reference (only a power
+ * cycle realigned it).
+ *
+ * The sequence mirrors the documented procedure: arm the on-demand PHFL and
+ * latch it with the PLL-page small-change update, then select the
+ * in-register phase trigger on Page 0 and pulse it. The Page 0 trigger
+ * register is touched read-modify-write so the unrelated OEb bits are
+ * preserved.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_output_phase_flush(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+ u8 ctrl, orig;
+ int rc, ret;
+
+ /* Arm the on-demand phase-flush on the PLL page. */
+ rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_PHFL_CTRL,
+ SIT9531X_PLL_PHFL_ON_DEMAND_EN,
+ SIT9531X_PLL_PHFL_ON_DEMAND_EN);
+ if (rc)
+ return rc;
+
+ /*
+ * Latch it with the PLL small-change update. 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_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_SMALL_UPDATE,
+ SIT9531X_SMALL_UPDATE_CMD);
+ if (rc)
+ goto disarm;
+
+ /*
+ * Select the in-register phase trigger, preserving the OEb bits.
+ * Remember the original register value (with the trigger de-asserted)
+ * so the trigger-source select can be restored once the pulse has
+ * fired.
+ */
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, &ctrl);
+ if (rc)
+ goto disarm;
+
+ orig = ctrl & ~SIT9531X_DIVO_PHASE_TRIG;
+ ctrl = orig | SIT9531X_DIVO_PHASE_SEL_REG;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, ctrl);
+ if (rc)
+ goto disarm;
+
+ /*
+ * Pulse the phase trigger. No explicit delay is needed between the
+ * set and clear writes: each I2C transaction takes far longer than
+ * any minimum pulse width.
+ */
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1,
+ ctrl | SIT9531X_DIVO_PHASE_TRIG);
+
+ /*
+ * Restore the original trigger-source select. The pulse above has
+ * already latched the flush, so a one-shot flush must not leave the
+ * phase trigger permanently pinned to the in-register source. This
+ * runs even when the pulse write failed, otherwise a failed flush
+ * would keep a hardware trigger source hijacked; the restore error
+ * is only surfaced when it would not mask the pulse failure.
+ */
+ ret = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, orig);
+ if (ret && !rc)
+ rc = ret;
+
+disarm:
+ /*
+ * Disarm the on-demand flush enable armed above. Leaving it set
+ * would let a later assertion of the restored trigger source
+ * re-flush every output divider of this PLL, which is exactly the
+ * persistent side effect the one-shot sequence must not have.
+ */
+ ret = sit9531x_update_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_PHFL_CTRL,
+ SIT9531X_PLL_PHFL_ON_DEMAND_EN, 0);
+ if (!ret)
+ ret = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_SMALL_UPDATE,
+ SIT9531X_SMALL_UPDATE_CMD);
+ if (ret && !rc)
+ rc = ret;
+
+ return rc;
+}
+
+/*
+ * sit9531x_output_divo_calc - work out an output's divider and its VCO
+ *
+ * Separated from the write so a caller that programs more than the
+ * divider in one sequence can compute the value before it enters the
+ * programming state.
+ */
+static int sit9531x_output_divo_calc(struct sit9531x_dev *sitdev, u8 out_idx,
+ u8 pll_idx, u64 frequency, u64 *fvco_out,
+ u64 *divo_out)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ u64 fvco, divo;
+ 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 board override verbatim and a
+ * register-derived rate clamped to the PLL's band, 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. With a band-clamped Fvco this cannot
+ * overflow, but a DT Fvco override is taken verbatim, so guard the
+ * field width rather than silently truncating the divider.
+ */
+ if (divo > SIT9531X_DIVO_MAX)
+ return -EINVAL;
+
+ /*
+ * 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:
+ for (j = 0; j < written; j++) {
+ rb_rc = sit9531x_write_u8(sitdev,
+ SIT9531X_REG(page, base_reg - j),
+ old_bytes[j]);
+ if (rb_rc) {
+ dev_err(sitdev->dev,
+ "out%u: DIVO rollback failed (%d), the divider is part old and part new\n",
+ out_idx, rb_rc);
+ if (!rc)
+ rc = rb_rc;
+ }
+ }
+
+ return rc;
+}
+
+int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
+ u8 pll_idx, u64 frequency)
+{
+ u64 fvco, divo;
+ int rc, ret;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ rc = sit9531x_output_divo_calc(sitdev, out_idx, pll_idx, frequency,
+ &fvco, &divo);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_prg_enter(sitdev);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_output_divo_write(sitdev, out_idx, divo);
+ /*
+ * Step 4: NVM update + loop lock. Always run prg_commit() so the chip
+ * leaves the PRG_CMD state with the output loops re-locked, even when a
+ * write above failed; keep the first error to return. It also carries
+ * the required post-lock settling sleep.
+ */
+ ret = sit9531x_prg_commit(sitdev);
+ if (ret && !rc)
+ rc = ret;
+ if (rc)
+ return rc;
+
+ /*
+ * Step 5: flush the PLL's output phase so the new DIVO starts
+ * aligned instead of keeping the arbitrary phase the divider
+ * happened to be at.
+ */
+ /*
+ * 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, -ENODEV when the output divider or VCO rate
+ * is not resolvable, <0 on register access error
+ */
+int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
+ u64 *frequency)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ u8 slot, page, base_reg, pll_idx, v;
+ u64 fvco, divo = 0;
+ int rc, j;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (out_idx >= info->num_outputs)
+ return -EINVAL;
+
+ pll_idx = sitdev->out[out_idx].pll_idx;
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return -ENODEV;
+
+ rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
+ if (rc)
+ return rc == -ENODATA ? -ENODEV : rc;
+
+ slot = info->clkout_map[out_idx];
+ if (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX)
+ page = SIT9531X_PAGE_OUTSYS1;
+ else
+ page = SIT9531X_PAGE_OUTSYS0;
+ base_reg = clkout_odr_divn_base[slot % 6];
+
+ for (j = 4; j >= 0; j--) {
+ rc = sit9531x_read_u8(sitdev,
+ SIT9531X_REG(page, base_reg - j), &v);
+ if (rc)
+ return rc;
+ if (j == 4)
+ v &= 0x03;
+ divo = (divo << 8) | v;
+ }
+
+ if (!divo)
+ return -ENODEV;
+
+ *frequency = div64_u64(fvco, divo);
+ sitdev->out[out_idx].freq = *frequency;
+
+ return 0;
+}
+
/*
* Phase adjust (PRG_RST_DELAY register-based).
*
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 2c5d0100b450..91b84b420011 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -255,6 +255,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 56a8213dee07..9f0678ddfe0e 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -337,6 +337,28 @@ sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
return 0;
}
+/*
+ * sit9531x_dpll_input_pin_frequency_get - read input pin frequency
+ *
+ * returns cached frequency from DT or last set.
+ */
+static int
+sit9531x_dpll_input_pin_frequency_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, u64 *frequency,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ const struct sit9531x_ref *ref;
+
+ ref = sit9531x_ref_state_get(sitdpll->dev, dpin->id);
+ *frequency = ref->freq;
+
+ return 0;
+}
+
/*
* sit9531x_dpll_input_pin_state_on_dpll_get - get input pin DPLL state
*
@@ -618,6 +640,7 @@ sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
.direction_get = sit9531x_dpll_input_pin_direction_get,
+ .frequency_get = sit9531x_dpll_input_pin_frequency_get,
.state_on_dpll_get = sit9531x_dpll_input_pin_state_on_dpll_get,
.state_on_dpll_set = sit9531x_dpll_input_pin_state_on_dpll_set,
.prio_get = sit9531x_dpll_input_pin_prio_get,
@@ -676,6 +699,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,
};
@@ -691,8 +715,78 @@ sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
return 0;
}
+/*
+ * sit9531x_dpll_output_pin_frequency_get - read output pin frequency
+ *
+ * Reads the DIVO divider back from the chip and computes the live
+ * frequency as Fvco / DIVO. Falls back to the cached value only when
+ * the output is not resolvable through the divider chain (e.g. not
+ * mapped to a PLL), so transport/register errors still surface.
+ */
+static int
+sit9531x_dpll_output_pin_frequency_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, u64 *frequency,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_output_freq_get(sitdev, dpin->id, frequency);
+ if (rc == -ENODEV)
+ *frequency = sit9531x_out_state_get(sitdev, dpin->id)->freq;
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return rc == -ENODEV ? 0 : rc;
+}
+
+/*
+ * sit9531x_dpll_output_pin_frequency_set - set output pin frequency
+ *
+ * computes DIVO = Fvco / frequency and writes the
+ * 34-bit output divider to the output system registers via
+ * sit9531x_output_freq_set().
+ */
+static int
+sit9531x_dpll_output_pin_frequency_set(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, u64 frequency,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ u8 actual_pll;
+ int rc;
+
+ /*
+ * Read the PLL that drives this output from its OUT_MAP state
+ * (populated by out_state_fetch from the chip's OUT_MAP registers).
+ * That is the index the output register programming below is keyed
+ * by; the output is registered under the DPLL matching this PLL.
+ */
+ actual_pll = sitdev->out[dpin->id].pll_idx;
+
+ mutex_lock(&sitdev->multiop_lock);
+ rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll,
+ frequency);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc)
+ NL_SET_ERR_MSG(extack, "Output frequency set failed");
+
+ return rc;
+}
+
static const struct dpll_pin_ops sit9531x_dpll_output_pin_ops = {
.direction_get = sit9531x_dpll_output_pin_direction_get,
+ .frequency_get = sit9531x_dpll_output_pin_frequency_get,
+ .frequency_set = sit9531x_dpll_output_pin_frequency_set,
};
const struct dpll_pin_ops *
diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
index 4c6a2249300f..8270b8ee91be 100644
--- a/drivers/dpll/sit9531x/prop.c
+++ b/drivers/dpll/sit9531x/prop.c
@@ -295,16 +295,34 @@ sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
}
/*
- * Seed the runtime ref->freq / out->freq with the first DT-listed
- * supported frequency so the netlink frequency_get callback reports
- * a sane initial value before any pin_set occurs. DT lists the
- * physically-wired reference frequency for each input pin and the
- * default output frequency for each output pin.
+ * Seed the runtime ref->freq with the first DT-listed supported
+ * frequency: an input's rate is a board fact the device cannot be
+ * asked for, so firmware is the only source. An output is left to
+ * the read-back below, which knows what the divider is actually
+ * doing.
*/
- if (num_freqs > 0) {
- if (dir != DPLL_PIN_DIRECTION_INPUT ||
- index != SIT9531X_MAX_INPUTS)
- curr_freq = freqs[0];
+ 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:
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index 4d8eb3ceac9f..8ce048e9c8f1 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -192,6 +192,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.
@@ -206,6 +207,10 @@
/* ---- PLL page registers (apply to pages 0x0A-0x0D) ---- */
#define SIT9531X_PLL_REG_SMALL_UPDATE 0x0F
+/* On-demand phase-flush enable (PLL page reg 0x3D bit 7) */
+#define SIT9531X_PLL_REG_PHFL_CTRL 0x3D
+#define SIT9531X_PLL_PHFL_ON_DEMAND_EN BIT(7)
+
/*
* Loop-filter coefficients on PLL_PAGE regs 0x10-0x15 (3 normal +
* 3 fast-lock) are GUI/NVM-generated by the timing configurator and must not be
@@ -217,6 +222,18 @@
#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 carried as fixed point, and the unit the DPLL ABI wants the
+ * fractional frequency offset in. Equal in value, distinct in meaning.
+ */
+#define SIT9531X_DIVN_SCALE 1000000000000ULL
+#define SIT9531X_PPT_PER_UNIT 1000000000000ULL
+
#define SIT9531X_PLL_REG_ACTIVE 0x02
#define SIT9531X_PLL_ACTIVE_BIT BIT(0) /* PLL reached active state */
--
2.43.0
^ permalink raw reply [flat|nested] 28+ messages in thread* Re: [PATCH v10 08/14] dpll: sit9531x: add support to get and set frequency on pins
2026-09-21 20:11 ` [PATCH v10 08/14] dpll: sit9531x: add support to get and set frequency on pins Ali Rouhi
@ 2026-09-26 2:34 ` Jakub Kicinski
0 siblings, 0 replies; 28+ messages in thread
From: Jakub Kicinski @ 2026-09-26 2:34 UTC (permalink / raw)
To: arouhi
Cc: Jakub Kicinski, jiri, vadim.fedorenko, arkadiusz.kubalewski,
ivecera, robh, krzk+dt, conor+dt, cjubran, pabeni,
Oleg.Zadorozhnyi, prabhakar.mahadev-lad.rj, dev, devicetree,
netdev, linux-kernel
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
dpll: sit9531x: add support to get and set frequency on pins
This commit adds frequency_get for input and XO pins, which reports the
rate described in firmware. It also adds frequency_get and frequency_set
for output pins, where the rate is Fvco / DIVO and Fvco comes from the
PLL's DIVN registers and the crystal rate. A set writes a new DIVO inside
the output-system programming state and then does a per-PLL phase flush.
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index ac184c93258cb..0687ad6de861c 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -418,6 +418,127 @@ static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev,
[ ... ]
> +static int sit9531x_prg_commit(struct sit9531x_dev *sitdev)
> +{
> + int rc, rc2 = 0, rc3;
> + u8 attempt;
> +
> + rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
> + SIT9531X_UPDATE_NVM);
> +
> + /*
> + * Issue the loop lock even if the update failed. Callers reach
> + * this function through a goto so that the chip never stays in
> + * the PRG_CMD state with its loops open; returning early here
> + * would defeat that and leave the outputs unlocked until the
> + * next successful commit.
> + */
[Severity: Low]
This isn't a bug, but this comment says callers reach this function
through a goto. The only caller added here, sit9531x_output_freq_set(),
calls it directly after sit9531x_output_divo_write().
The kernel-doc of sit9531x_dpll_input_pin_frequency_get() has a similar
problem:
* returns cached frequency from DT or last set.
Neither sit9531x_dpll_input_pin_ops nor sit9531x_dpll_xo_pin_ops has a
.frequency_set, and ref[].freq is only written in prop.c at probe. So
there is never a "last set" value. That is still true at the end of the
series.
Could both comments be updated?
[ ... ]
> + rc3 = sit9531x_write_u8(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
> + SIT9531X_DEBUG_LOCK_VAL);
> +
> + if (rc)
> + return rc;
> +
> + return rc2 ? rc2 : rc3;
> +}
[Severity: Low]
Suppose UPDATE_NVM and LOOP_LOCK both succeed and only the final
SIT9531X_REG_OUTSYS_DEBUG write fails. This returns rc3, but the new
divider is already committed.
sit9531x_output_freq_set() then returns early:
ret = sit9531x_prg_commit(sitdev);
if (ret && !rc)
rc = ret;
if (rc)
return rc;
That skips the phase flush and the out[].freq update. At the end of the
series it also skips the phase-adjust re-time.
On an identical retry, dpll_pin_freq_set() calls frequency_get first. It
reads the new rate back from hardware, sees freq == old_freq, and returns
0 without calling the setter. Does that mean the skipped flush never runs?
sit9531x_output_freq_set() already downgrades a failed flush to a warning
for this reason ("the core asks for the current rate first and would drop
an identical retry"). Should a failure of only the debug-key write be
handled the same way?
[ ... ]
> @@ -1026,6 +1152,561 @@ int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
[ ... ]
> +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);
[ ... ]
> + fref = (u64)sitdev->xtal_freq << doubler;
> +
> + *fvco = mul_u64_u64_div_u64(fref, (u64)divn, SIT9531X_DIVN_SCALE);
[Severity: Medium]
Can this double truncation leave fvco 1 Hz low?
sit9531x_divn_fixed() floors the fraction, and this multiply floors it
again. Take fref = 48 MHz and DIVN = 104 + 1/6 (NUM 1, DEN raw 5). The true
VCO is exactly 5,000,000,000 Hz, but the code computes:
frac = 166,666,666,666
divn = 104,166,666,666,666
fvco = floor(4,999,999,999.999968) = 4,999,999,999
That is inside the low band, so the clamp below does not apply.
For a 10 MHz request, sit9531x_output_divo_calc() computes divo = 500.
Then this check:
if (div64_u64(fvco, divo) != frequency) {
compares 9,999,999 with 10,000,000 and returns -EINVAL, even though
DIVO = 500 gives exactly 10 MHz.
sit9531x_output_freq_get() would also report and cache 9,999,999 Hz, and
prop.c advertises that as the current rate. Meanwhile a DT-listed 10 MHz
passes the core's supported-frequency check and then fails in the set.
This applies whenever the DIVN denominator has prime factors other than 2
and 5. It is unchanged at the end of the series, and only a DT pll-fvco
override avoids it.
> + if (*fvco < fvco_min)
> + *fvco = fvco_min;
> + else if (*fvco > fvco_max)
> + *fvco = fvco_max;
> +
> + return 0;
> +}
[Severity: Medium]
When the register-derived Fvco falls outside the band, this returns
success with a band-edge value the VCO is not running at. Doesn't that
defeat the exactness check in sit9531x_output_divo_calc()?
For example, take a high-band PLL (B or D) whose Fref * DIVN is 5.0 GHz.
The clamp raises fvco to 6.875 GHz. A 12.5 MHz request gives DIVO = 550,
and this check passes because it compares against the same clamped value:
if (div64_u64(fvco, divo) != frequency) {
sit9531x_output_freq_set() commits the divider and returns 0. The output
actually runs at about 5.0e9 / 550, roughly 9.09 MHz.
The clamp is in the shared accessor, so sit9531x_output_freq_get() reports
12.5 MHz as well. Userspace has no way to see the mismatch.
The comment in sit9531x_output_divo_calc() says "programming a divider
from a guessed rate would put the output far from what was asked for while
reporting success". That seems to be what happens in this case.
The final patch in the series adds sit9531x_fvco_in_band(), but only uses
it to validate the DT override. This clamp is still there at the end of
the series.
[ ... ]
> +static int sit9531x_output_phase_flush(struct sit9531x_dev *sitdev, u8 pll_idx)
> +{
> + u8 ctrl, orig;
> + int rc, ret;
> +
> + /* Arm the on-demand phase-flush on the PLL page. */
> + rc = sit9531x_update_pll_u8(sitdev, pll_idx,
> + SIT9531X_PLL_REG_PHFL_CTRL,
> + SIT9531X_PLL_PHFL_ON_DEMAND_EN,
> + SIT9531X_PLL_PHFL_ON_DEMAND_EN);
> + if (rc)
> + return rc;
[ ... ]
> + rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1,
> + ctrl | SIT9531X_DIVO_PHASE_TRIG);
[Severity: Medium]
SIT9531X_REG_GPIO_FUNC_CTRL1 is SIT9531X_REG(0x00, 0x65), a chip-global
Page 0 register. Which PLLs respond to this pulse depends on each PLL's
own PHFL_CTRL bit 7.
This function arms that bit only for pll_idx. It never checks or disarms
it on the other three PLLs.
If the loaded profile left on-demand phase flush enabled on another PLL,
would a frequency set on one DPLL also step the phase of outputs that
belong to other DPLLs, with no notification?
That is wider than what the commit message describes:
The phase flush that follows a divider write realigns every output fed by
that PLL, not only the one that changed.
[ ... ]
> +disarm:
[ ... ]
> + ret = sit9531x_update_pll_u8(sitdev, pll_idx,
> + SIT9531X_PLL_REG_PHFL_CTRL,
> + SIT9531X_PLL_PHFL_ON_DEMAND_EN, 0);
> + if (!ret)
> + ret = sit9531x_write_pll_u8(sitdev, pll_idx,
> + SIT9531X_PLL_REG_SMALL_UPDATE,
> + SIT9531X_SMALL_UPDATE_CMD);
[Severity: Medium]
The arm step at the top of this function sets
SIT9531X_PLL_PHFL_ON_DEMAND_EN with regmap_update_bits() without saving the
previous value. This path always clears it and latches the change with
SIT9531X_SMALL_UPDATE_CMD.
Suppose the loaded NVM profile had on-demand phase flush enabled on this
PLL, for example on a board that realigns outputs from a GPIO trigger.
Would the first output frequency set on that PLL turn it off for good, so
that later GPIO-triggered flushes silently do nothing?
This function already saves and restores GPIO_FUNC_CTRL1 through orig.
Should PHFL_CTRL be saved and restored the same way?
[ ... ]
> + for (j = 0; j < 5; j++) {
> + rc = sit9531x_write_u8(sitdev,
> + SIT9531X_REG(page, base_reg - j),
> + divo_bytes[j]);
> + if (rc)
> + goto rollback;
> + written++;
> + }
> +
> + return 0;
> +
> +rollback:
> + for (j = 0; j < written; j++) {
[Severity: Medium]
written is only incremented after a successful write, so the byte whose
write reported failure is never restored here.
sit9531x_prg_enter() notes that "a transfer that reported an error may
still have reached the part". If that byte did land,
sit9531x_output_freq_set() still calls sit9531x_prg_commit()
unconditionally, and that issues UPDATE_NVM and LOOP_LOCK.
Can that commit a 34-bit divider that is part old and part new? The output
would then run at neither the old rate nor the requested one, while the
set returns an error and out[].freq keeps the old value.
The comment above this function says "so the output keeps the divider it
had rather than a mixture of the two". Should the rollback also restore
old_bytes[written]?
[ ... ]
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index 56a8213dee077..9f0678ddfe0ee 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
[ ... ]
> @@ -691,8 +715,78 @@ sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
[ ... ]
> + mutex_lock(&sitdev->multiop_lock);
> + rc = sit9531x_output_freq_get(sitdev, dpin->id, frequency);
> + if (rc == -ENODEV)
> + *frequency = sit9531x_out_state_get(sitdev, dpin->id)->freq;
> + mutex_unlock(&sitdev->multiop_lock);
> +
> + return rc == -ENODEV ? 0 : rc;
> +}
[Severity: Medium]
Is -ENODEV reliable as the "not resolvable" sentinel here?
sit9531x_output_freq_get() returns -ENODEV itself when divo is zero, and
here:
rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
if (rc)
return rc == -ENODATA ? -ENODEV : rc;
It also passes register access errors from sit9531x_read_u8(),
sit9531x_divn_static() and sit9531x_is_xo_doubler_enabled() through
unchanged.
The driver says elsewhere that the I2C layer returns -ENODEV for an
adapter that has gone away. The comment in
sit9531x_dpll_input_pin_phase_offset_get() reads: "Every other errno,
-ENODEV from a vanished adapter included, is a failure."
In that case a bus -ENODEV is reported here as a successful read of the
cached rate. That contradicts the comment above this function, "so
transport/register errors still surface".
It also affects frequency set, because the core reads the old rate first:
drivers/dpll/dpll_netlink.c:dpll_pin_freq_set() {
...
ret = ops->frequency_get(pin, dpll_pin_on_dpll_priv(dpll, pin), dpll,
dpll_priv(dpll), &old_freq, extack);
...
if (freq == old_freq)
return 0;
...
}
If the stale cached value equals the requested rate, the set reports
success without touching the hardware. This is still present at the end
of the series.
Could sit9531x_output_freq_get() use an errno for the unresolvable case
that a bus error cannot also produce?
[ ... ]
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH v10 09/14] dpll: sit9531x: implement output pin state on a DPLL
2026-09-21 20:11 [PATCH v10 00/14] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (7 preceding siblings ...)
2026-09-21 20:11 ` [PATCH v10 08/14] dpll: sit9531x: add support to get and set frequency on pins Ali Rouhi
@ 2026-09-21 20:11 ` Ali Rouhi
2026-09-26 2:34 ` Jakub Kicinski
2026-09-21 20:11 ` [PATCH v10 11/14] dpll: sit9531x: add support to get phase offset on the connected input pin Ali Rouhi
` (4 subsequent siblings)
13 siblings, 1 reply; 28+ messages in thread
From: Ali Rouhi @ 2026-09-21 20:11 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi,
prabhakar.mahadev-lad.rj, dev, devicetree, netdev, linux-kernel,
Ali Rouhi
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
An output is driven by its PLL rather than selected by it, so its state
says whether it carries a signal: connected while it drives, disconnected
while it is muted. Setting the state mutes or un-mutes it by forcing the
pad to Hi-Z, the only per-output control the device offers that leaves the
divider alone.
The force bit and the state bit are separate, and a pad follows the loaded
configuration while the force bit is clear, so both are read to decide
whether an output is muted and both are written to change it. Which of
the four register banks applies depends on the slot and on whether the pad
is single-ended or differential.
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>
---
Notes:
Changes in v10:
Write the forced-Hi-Z pair value first and override second, so the pad
is never pinned driven between the two, and release the half that
reached the device when the other did not.
Said in the changelog that this is where output pins start being
registered.
drivers/dpll/sit9531x/core.c | 249 ++++++++++++++++++++++++++++++++++-
drivers/dpll/sit9531x/core.h | 7 +
drivers/dpll/sit9531x/dpll.c | 87 ++++++++++++
3 files changed, 337 insertions(+), 6 deletions(-)
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 0687ad6de861..8daf0fbf6772 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -418,6 +418,47 @@ static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev,
return 0;
}
+/*
+ * sit9531x_output_state_refresh - read an output's mute state back
+ *
+ * Used when a mute could not be confirmed at the time it was written. The
+ * driver does not poll output state, so without this the cached value would
+ * stand until something else happened to write it.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_output_state_refresh(struct sit9531x_dev *sitdev, u8 out_idx)
+{
+ bool muted;
+ int rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ rc = sit9531x_output_forced_hiz(sitdev, out_idx, &muted);
+ if (rc)
+ return rc;
+
+ sitdev->out[out_idx].enabled = !muted;
+ sitdev->out[out_idx].state_stale = false;
+
+ return 0;
+}
+
+static int sit9531x_hiz_set_bit(struct sit9531x_dev *sitdev,
+ unsigned int reg, u8 bit, bool set)
+{
+ u8 cur, new_val;
+ int rc;
+
+ rc = sit9531x_read_u8(sitdev, reg, &cur);
+ if (rc)
+ return rc;
+
+ new_val = set ? (cur | BIT(bit)) : (cur & ~BIT(bit));
+
+ return sit9531x_write_u8(sitdev, reg, new_val);
+}
+
/* Attempts to re-lock the output loops before reporting them open. */
#define SIT9531X_LOOP_LOCK_TRIES 3
@@ -539,6 +580,190 @@ static int sit9531x_prg_commit(struct sit9531x_dev *sitdev)
return rc2 ? rc2 : rc3;
}
+/*
+ * sit9531x_output_hiz_write - mute or unmute an output
+ *
+ * Muting takes control of the pin (MASK=1) and drives it low (STATE=0) on
+ * both the differential and the single-ended register pair, because the
+ * output must go quiet whichever way it is wired; unmuting hands it back
+ * to the device's own state machine. The caller must already be in the
+ * programming state.
+ */
+static int sit9531x_output_hiz_write(struct sit9531x_dev *sitdev, u8 slot,
+ bool mute)
+{
+ struct sit9531x_hiz_regs r;
+ int rc, undo_rc;
+
+ sit9531x_output_get_hiz_regs(slot, &r);
+
+ if (!mute) {
+ rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, false);
+ if (rc)
+ return rc;
+
+ return sit9531x_hiz_set_bit(sitdev, r.se_mask, r.bit, false);
+ }
+
+ /*
+ * 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.
+ */
+ rc = sit9531x_hiz_set_bit(sitdev, r.diff_state, r.bit, false);
+ if (rc)
+ return rc;
+ rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, true);
+ if (rc)
+ return rc;
+ rc = sit9531x_hiz_set_bit(sitdev, r.se_state, r.bit, false);
+ if (rc)
+ goto undo_diff;
+ rc = sit9531x_hiz_set_bit(sitdev, r.se_mask, r.bit, true);
+ if (rc)
+ goto undo_diff;
+
+ return 0;
+
+undo_diff:
+ /*
+ * Only one half of the pair reached the device. Release the
+ * override that did: that leaves the pad on the state the loaded
+ * configuration gave it, which is where the request started, rather
+ * than driven by half a mute that nothing afterwards clears.
+ */
+ undo_rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, false);
+ if (undo_rc)
+ dev_err(sitdev->dev,
+ "slot%u: Hi-Z override left half 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 BOTH the DIFF and SE register pairs so that the
+ * output is muted regardless of its electrical configuration. The
+ * writes are wrapped in the PRG_CMD / NVM update / loop lock sequence
+ * so the new state is applied by the hardware.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_output_disable(struct sit9531x_dev *sitdev, u8 index)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ bool muted;
+ u8 slot;
+ int rc, ret, state_rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (index >= info->num_outputs)
+ return -EINVAL;
+
+ slot = info->clkout_map[index];
+ rc = sit9531x_prg_enter(sitdev);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_output_hiz_write(sitdev, slot, true);
+
+ /*
+ * Always leave the PRG_CMD programming state, even on a mid-sequence
+ * write failure: prg_enter() unlocked the output loops, so returning
+ * without prg_commit() would strand the chip in the programming state
+ * with the loops unlocked. Best effort -- keep the first error.
+ */
+ ret = sit9531x_prg_commit(sitdev);
+ if (ret && !rc)
+ rc = ret;
+
+ /*
+ * Keep the software state aligned to what hardware now drives even
+ * when one write in the sequence failed. The commit above may have
+ * applied a partial mask/state combination.
+ */
+ state_rc = sit9531x_output_forced_hiz(sitdev, index, &muted);
+ if (!state_rc) {
+ sitdev->out[index].enabled = !muted;
+ sitdev->out[index].state_stale = false;
+ } else {
+ /*
+ * The writes may well have landed; what failed is the proof.
+ * Mark the cached state for a read-through rather than
+ * reporting the value it had before this call.
+ */
+ sitdev->out[index].state_stale = true;
+ if (!rc)
+ rc = state_rc;
+ }
+
+ return rc;
+}
+
+/*
+ * sit9531x_output_enable - un-mute an output (active state)
+ * @index: logical output index (0..info->num_outputs-1)
+ *
+ * Releases MASK on BOTH register pairs so the output returns to
+ * whatever the initial_config blob programmed. The writes are wrapped
+ * in the PRG_CMD / NVM update / loop lock sequence so the new state is
+ * applied by the hardware.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_output_enable(struct sit9531x_dev *sitdev, u8 index)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ bool muted;
+ u8 slot;
+ int rc, ret, state_rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (index >= info->num_outputs)
+ return -EINVAL;
+
+ slot = info->clkout_map[index];
+ rc = sit9531x_prg_enter(sitdev);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_output_hiz_write(sitdev, slot, false);
+
+ /*
+ * Always leave the PRG_CMD programming state, even on a mid-sequence
+ * write failure: prg_enter() unlocked the output loops, so returning
+ * without prg_commit() would strand the chip in the programming state
+ * with the loops unlocked. Best effort -- keep the first error.
+ */
+ ret = sit9531x_prg_commit(sitdev);
+ if (ret && !rc)
+ rc = ret;
+
+ /* See sit9531x_output_disable(): commit can apply 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
*
@@ -1997,6 +2222,8 @@ static int sit9531x_out_state_fetch(struct sit9531x_dev *sitdev, u8 index)
if (rc)
return rc;
+ sitdev->out[index].state_stale = false;
+
/*
* The OUT_MAP_LO/HI bitmaps are indexed by the physical slot the
* output occupies on the chip, not by the driver's logical output
@@ -2655,8 +2882,8 @@ static bool sit9531x_input_pin_is_registrable(struct sit9531x_dev *sitdev,
* @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
*/
@@ -2666,13 +2893,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 91b84b420011..3b35bf1c4cd5 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -97,6 +97,8 @@ struct sit9531x_ref {
* @enabled: output is driving, i.e. not forced into Hi-Z
* @cmos: output is wired single-ended; the Hi-Z pair that
* speaks for it is the SE one, not the differential
+ * @state_stale: the cached mute state could not be confirmed against
+ * hardware and has to be read back before it is reported
* @routed: output is mapped to @pll_idx by the initial
* configuration; an unrouted output has no DPLL pin
* @pll_idx: PLL driving this output (0-3)
@@ -106,6 +108,7 @@ struct sit9531x_out {
u64 freq;
bool enabled;
bool cmos;
+ bool state_stale;
bool routed;
u8 pll_idx;
const char *label;
@@ -253,6 +256,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 9f0678ddfe0e..c1ab202bf297 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -783,10 +783,97 @@ 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 pin via the Page 0x03
+ * force/state register pair.
+ * CONNECTED -> enable (release force, back to factory default)
+ * DISCONNECTED -> disable (force Hi-Z)
+ */
+static int
+sit9531x_dpll_output_pin_state_on_dpll_set(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ switch (state) {
+ case DPLL_PIN_STATE_CONNECTED:
+ rc = sit9531x_output_enable(sitdev, dpin->id);
+ break;
+ case DPLL_PIN_STATE_DISCONNECTED:
+ rc = sit9531x_output_disable(sitdev, dpin->id);
+ break;
+ default:
+ rc = -EINVAL;
+ break;
+ }
+
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc)
+ NL_SET_ERR_MSG(extack, "Failed to set output pin state");
+
+ return rc;
+}
+
static const struct dpll_pin_ops sit9531x_dpll_output_pin_ops = {
.direction_get = sit9531x_dpll_output_pin_direction_get,
.frequency_get = sit9531x_dpll_output_pin_frequency_get,
.frequency_set = sit9531x_dpll_output_pin_frequency_set,
+ .state_on_dpll_get = sit9531x_dpll_output_pin_state_on_dpll_get,
+ .state_on_dpll_set = sit9531x_dpll_output_pin_state_on_dpll_set,
};
const struct dpll_pin_ops *
--
2.43.0
^ permalink raw reply [flat|nested] 28+ messages in thread* Re: [PATCH v10 09/14] dpll: sit9531x: implement output pin state on a DPLL
2026-09-21 20:11 ` [PATCH v10 09/14] dpll: sit9531x: implement output pin state on a DPLL Ali Rouhi
@ 2026-09-26 2:34 ` Jakub Kicinski
0 siblings, 0 replies; 28+ messages in thread
From: Jakub Kicinski @ 2026-09-26 2:34 UTC (permalink / raw)
To: arouhi
Cc: Jakub Kicinski, jiri, vadim.fedorenko, arkadiusz.kubalewski,
ivecera, robh, krzk+dt, conor+dt, cjubran, pabeni,
Oleg.Zadorozhnyi, prabhakar.mahadev-lad.rj, dev, devicetree,
netdev, linux-kernel
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
dpll: sit9531x: implement output pin state on a DPLL
This adds state_on_dpll_get/set callbacks for sit9531x output pins. An
output is muted or un-muted by forcing its pad through the Hi-Z mask/state
register pairs, on both the differential and the single-ended pair. It
also starts registering output pins, which were previously filtered out.
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index 0687ad6de861c..8daf0fbf67720 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
[ ... ]
> @@ -539,6 +580,190 @@ static int sit9531x_prg_commit(struct sit9531x_dev *sitdev)
> return rc2 ? rc2 : rc3;
> }
>
> +/*
> + * sit9531x_output_hiz_write - mute or unmute an output
> + *
> + * Muting takes control of the pin (MASK=1) and drives it low (STATE=0) on
> + * both the differential and the single-ended register pair, because the
> + * output must go quiet whichever way it is wired; unmuting hands it back
> + * to the device's own state machine. The caller must already be in the
> + * programming state.
> + */
[Severity: Low]
This isn't a bug, but the comments added here don't agree on what a mute
does. This comment, and the header of sit9531x_output_forced_hiz(), say
muting "drives it low (STATE=0)". The register block comment above
sit9531x_output_get_hiz_regs() says:
* STATE bit = 0 -> output is forced to Hi-Z (muted)
sit9531x_output_disable() ("force Hi-Z"), the @enabled kerneldoc ("not
forced into Hi-Z") and sit9531x_dpll_output_pin_state_on_dpll_set()
("forces Hi-Z") also describe Hi-Z. Is the pad driven low or Hi-Z?
The comments also disagree on what un-muting restores.
sit9531x_output_enable() says releasing MASK returns the output "to whatever
the initial_config blob programmed". The set callback in dpll.c says "back
to factory default". If the blob set MASK itself, which the
sit9531x_output_forced_hiz() header allows for, clearing MASK doesn't
restore what the blob programmed.
In dpll.c, the set callback comment mentions "the Page 0x03 force/state
register pair" (singular), but the code writes both the DIFF and the SE
pairs. The get callback comment says DISCONNECTED means "muted via
sit9531x_output_disable()". However, sit9531x_out_state_fetch() also
reports DISCONNECTED for a mute that the blob set.
> +static int sit9531x_output_hiz_write(struct sit9531x_dev *sitdev, u8 slot,
> + bool mute)
> +{
> + struct sit9531x_hiz_regs r;
> + int rc, undo_rc;
> +
> + sit9531x_output_get_hiz_regs(slot, &r);
> +
> + if (!mute) {
> + rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, false);
> + if (rc)
> + return rc;
> +
> + return sit9531x_hiz_set_bit(sitdev, r.se_mask, r.bit, false);
> + }
> +
[ ... ]
> + rc = sit9531x_hiz_set_bit(sitdev, r.diff_state, r.bit, false);
> + if (rc)
> + return rc;
> + rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, true);
> + if (rc)
> + return rc;
> + rc = sit9531x_hiz_set_bit(sitdev, r.se_state, r.bit, false);
> + if (rc)
> + goto undo_diff;
> + rc = sit9531x_hiz_set_bit(sitdev, r.se_mask, r.bit, true);
> + if (rc)
> + goto undo_diff;
> +
> + return 0;
> +
> +undo_diff:
> + /*
> + * Only one half of the pair reached the device. Release the
> + * override that did: that leaves the pad on the state the loaded
> + * configuration gave it, which is where the request started, rather
> + * than driven by half a mute that nothing afterwards clears.
> + */
> + undo_rc = sit9531x_hiz_set_bit(sitdev, r.diff_mask, r.bit, false);
[Severity: High]
Can this rollback un-mute an output that was already muted?
undo_diff always clears the DIFF MASK bit. It never saves the DIFF
MASK/STATE bits the slot held on entry, so it can't restore them.
Take a differential output (cmos=false) that is already muted, with DIFF
MASK=1 and STATE=0. The mute can come from an earlier DISCONNECTED request
or from the loaded configuration; sit9531x_out_state_fetch() reports either
as DISCONNECTED. A repeated DISCONNECTED request then does this:
dpll_pin_state_set()
sit9531x_dpll_output_pin_state_on_dpll_set()
sit9531x_output_disable()
sit9531x_prg_enter()
sit9531x_output_hiz_write()
DIFF state/mask writes succeed (no change)
SE state or mask read/write fails
undo_diff clears DIFF MASK
sit9531x_prg_commit() <- applies MASK=0
sit9531x_output_forced_hiz() <- enabled = true
The request returns an error, but the pad has gone from muted to driving.
Nothing blocks this path. dpll_pin_state_set() forwards every request
without comparing it with the current state. Neither
sit9531x_dpll_output_pin_state_on_dpll_set() nor sit9531x_output_disable()
checks out[index].enabled first.
The same happens if the loaded configuration forced the pad driven (MASK=1,
STATE=1). The undo leaves MASK=0 and STATE=0, which is not where the
request started.
The comment above says the pad is left "where the request started". The v10
notes say this releases "the half that reached the device when the other did
not". Do either of these hold when MASK was already set on entry?
On a differential output the SE pair doesn't control the pad. Even so, a
failure on the SE pair releases the DIFF override that does control it.
Would it work to snapshot the original DIFF MASK/STATE bits before the
writes and restore them here, instead of clearing MASK unconditionally?
> + if (undo_rc)
> + dev_err(sitdev->dev,
> + "slot%u: Hi-Z override left half applied (%d)\n",
> + slot, undo_rc);
> +
> + return rc;
> +}
[ ... ]
> +int sit9531x_output_disable(struct sit9531x_dev *sitdev, u8 index)
> +{
[ ... ]
> + rc = sit9531x_output_hiz_write(sitdev, slot, true);
[ ... ]
> + 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);
[Severity: Low]
This isn't a bug, but the header of sit9531x_output_forced_hiz(), which
this function and sit9531x_output_state_refresh() now use for their
read-back, still says:
* it low (STATE bit clear). Either register pair muting the slot counts,
* mirroring what sit9531x_output_disable() programs.
The body reads only one pair, chosen by sitdev->out[out_idx].cmos. Its own
inline comment rejects the either-pair approach.
Take a differential output where the SE half of a mute landed and the DIFF
half did not. The header predicts muted, but the body returns not muted.
Could the header be updated to match the body?
[ ... ]
> +int sit9531x_output_enable(struct sit9531x_dev *sitdev, u8 index)
> +{
[ ... ]
> + rc = sit9531x_output_hiz_write(sitdev, slot, false);
[ ... ]
> + 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 {
[ ... ]
> + }
> +
> + return rc;
> +}
[Severity: Medium]
Is a state change that lands on the hardware during a failed request ever
announced to netlink subscribers?
Take a muted differential output. sit9531x_output_hiz_write() can clear
DIFF MASK and then fail on the SE mask read or write. prg_commit() still
runs, and the read-back above sets enabled = true and state_stale = false.
The original error is still returned.
dpll_pin_state_set() then returns before sending the notification:
ret = ops->state_on_dpll_set(pin, dpll_pin_on_dpll_priv(dpll, pin),
dpll, dpll_priv(dpll), state, extack);
if (ret)
return ret;
__dpll_pin_change_ntf(pin);
The periodic sit9531x_dpll_changes_check() doesn't catch it later either,
because it skips output pins:
if (!sit9531x_dpll_is_input_pin(pin) ||
sit9531x_dpll_is_xo_pin(pin))
continue;
The same gap applies to the un-mute caused by the undo_diff rollback in
sit9531x_output_disable(). It also applies to a DISCONNECTED request whose
rollback write failed as well.
Wouldn't subscribers keep seeing the old pin state indefinitely, while the
hardware and the driver's own cache report the new one?
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH v10 11/14] dpll: sit9531x: add support to get phase offset on the connected input pin
2026-09-21 20:11 [PATCH v10 00/14] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (8 preceding siblings ...)
2026-09-21 20:11 ` [PATCH v10 09/14] dpll: sit9531x: implement output pin state on a DPLL Ali Rouhi
@ 2026-09-21 20:11 ` Ali Rouhi
2026-09-26 2:34 ` Jakub Kicinski
2026-09-21 20:11 ` [PATCH v10 10/14] dpll: sit9531x: add support to adjust output phase Ali Rouhi
` (3 subsequent siblings)
13 siblings, 1 reply; 28+ messages in thread
From: Ali Rouhi @ 2026-09-21 20:11 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi,
prabhakar.mahadev-lad.rj, dev, devicetree, netdev, linux-kernel,
Ali Rouhi
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Report the phase difference between a PLL's reference and the PLL itself,
which is the loop's own residual error and therefore trends small on a
locked device -- that is the measurement, not an artefact of it.
The value comes from the on-chip time-to-digital converter, read through
the debug window: unlock the window, point it at the converter, then read
the trigger register, which latches a fresh sample and returns the
previous one. It is read three times per sample for that reason; a single
read hands back the sample from the last call, so a repeated measurement
would look perfectly steady while saying nothing.
Only the input a PLL has actually selected has a phase offset against it.
For any other pin there is nothing to measure and zero is reported,
because the core abandons an entire pin dump on an error from any one pin.
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
Notes:
Changes in v10:
Gave the dormant-PLL case its own errno, so a bus error is not reported
as a valid zero reading.
Read the selected reference from the device for the measurement rather
than from a cache up to a poll period old.
Restored the debug tap selection the read changes, and dropped the
cached per-pin phase offset, which was written on every path of the
getter and read on none.
drivers/dpll/sit9531x/core.c | 215 +++++++++++++++++++++++++++++++++++
drivers/dpll/sit9531x/core.h | 4 +
drivers/dpll/sit9531x/dpll.c | 118 +++++++++++++++++++
drivers/dpll/sit9531x/regs.h | 36 ++++++
4 files changed, 373 insertions(+)
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index c8c3cd6a64ba..8f6f8ffc8dc1 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -2314,6 +2314,221 @@ 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;
+
+ *ref = sit9531x_hw_src_input(input_sel & SIT9531X_PRIO_NIBBLE_MASK);
+
+ return 0;
+}
+
+/*
+ * sit9531x_phase_offset_read - read phase difference via TDC
+ * @phase_ps: output phase difference in picoseconds
+ *
+ * Reads the Time-to-Digital Converter (TDC) signed 35-bit code from the
+ * PLL page registers, then converts to picoseconds using the VCO
+ * frequency: phase_diff = tdc_code / fvco.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_phase_offset_read(struct sit9531x_dev *sitdev, u8 pll_idx,
+ s64 *phase_ps)
+{
+ u8 v, old_write_code, old_read_code;
+ bool have_old = false;
+ int rc, lock_rc, 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;
+
+ /* 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.
+ */
+ if (!sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_WRITE_CODE,
+ &old_write_code) &&
+ !sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_READ_CODE,
+ &old_read_code))
+ 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;
+
+ /*
+ * Get VCO frequency for conversion. -ENODATA means DIVN is not
+ * programmed (PLL unused on this board) -- skip silently rather
+ * than spamming the log on every poll cycle. It is passed up as
+ * itself rather than as -ENODEV, which the I2C layer produces for
+ * an adapter that has gone away: the caller turns the dormant-PLL
+ * case into a zero reading, and a bus failure must not take that
+ * path.
+ */
+ 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);
+ goto relock;
+ }
+
+ /*
+ * phase_diff (seconds) = tdc_code / fvco
+ * phase_diff (ps) = tdc_code * 1e12 / fvco
+ *
+ * mul_u64_u64_div_u64() keeps the exact Hz denominator; dividing
+ * by whole MHz instead would lose up to ~40 ppm of scale on a
+ * fractional-DIVN Fvco.
+ */
+ mag_ps = mul_u64_u64_div_u64(tdc_signed < 0 ? -tdc_signed : tdc_signed,
+ 1000000000000ULL, fvco);
+ *phase_ps = tdc_signed < 0 ? -(s64)mag_ps : (s64)mag_ps;
+
+ rc = 0;
+
+relock:
+ if (have_old) {
+ sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_READ_CODE,
+ old_read_code);
+ sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_WRITE_CODE,
+ old_write_code);
+ }
+
+ /*
+ * Close the debug window again. The key register opens every debug
+ * register on this PLL while it holds the unlock value, and this read
+ * runs on every pin-get of a connected input, so leaving it open
+ * would mean normal monitoring permanently unlocks the block.
+ */
+ lock_rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DEBUG,
+ SIT9531X_PLL_DEBUG_LOCK);
+ if (lock_rc && !rc)
+ rc = lock_rc;
+
+ return rc;
+}
+
/*
* sit9531x_ref_state_fetch - read input reference status from hardware
* @index: logical input index
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 7848ac9bd6ca..f2750c319ff3 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -289,6 +289,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 af1089f192b6..5f7c2d01562c 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -638,6 +638,123 @@ 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 confirms this is
+ * a property of the hardware rather than of their measurement script.
+ * The script satisfies it by mapping a spare output and restarting the
+ * PLL -- side effects that do not belong in a getter, so a reading taken
+ * in that state is simply not meaningful.
+ *
+ * Non-selected pins and a PLL with no programmed divider report zero
+ * rather than an error: the DPLL core propagates any error from this
+ * callback and fails the whole pin dump with it, unlike the frequency
+ * offset getter, where -ENODATA makes the core omit the attribute. There
+ * is no per-pin "no data" for phase offset, so it is a value or no
+ * callback at all.
+ */
+static int
+sit9531x_dpll_input_pin_phase_offset_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, s64 *phase_offset,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ enum dpll_pin_state state;
+ s64 offset;
+ u8 selected;
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ /*
+ * The on-chip TDC is a per-PLL resource that always measures the
+ * phase difference between the VCO and the PLL's currently
+ * selected reference; it cannot be pointed at an arbitrary input.
+ * For any input that is not the active reference there is no
+ * meaningful per-pin phase offset, so report 0 instead of the
+ * active reference's value.
+ */
+ /*
+ * Which pin the sample belongs to is read from the device rather
+ * than taken from the monitor's cache: the device selects its own
+ * reference, so a cache up to a poll period old could attribute a
+ * live measurement to the pin that used to be selected.
+ */
+ rc = sit9531x_chan_selected_ref_read(sitdev, sitdpll->id,
+ &selected);
+ if (rc) {
+ mutex_unlock(&sitdev->multiop_lock);
+ NL_SET_ERR_MSG(extack,
+ "Selected reference could not be read back");
+ return rc;
+ }
+
+ sit9531x_dpll_selection_state_get(sitdev, sitdpll, dpin->id, &state);
+ if (state != DPLL_PIN_STATE_CONNECTED || selected != dpin->id) {
+ mutex_unlock(&sitdev->multiop_lock);
+ *phase_offset = 0;
+ return 0;
+ }
+
+ rc = sit9531x_phase_offset_read(sitdev, sitdpll->id, &offset);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ /*
+ * -ENODATA means the PLL has no programmed DIVN (unused on this
+ * board); report phase_offset = 0 so a full pin-get dump does not
+ * fail just because one DPLL is dormant. 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,
@@ -645,6 +762,7 @@ static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
.state_on_dpll_set = sit9531x_dpll_input_pin_state_on_dpll_set,
.prio_get = sit9531x_dpll_input_pin_prio_get,
.prio_set = sit9531x_dpll_input_pin_prio_set,
+ .phase_offset_get = sit9531x_dpll_input_pin_phase_offset_get,
};
/*
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index cbce62404c97..a9731c360bc0 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -255,6 +255,42 @@
#define SIT9531X_PLL_REG_DIVN_NUM 0x32 /* 4 bytes (0x32-0x35) */
#define SIT9531X_PLL_REG_DIVN_DEN 0x38 /* 4 bytes (0x38-0x3B) */
+/* Debug register unlock */
+#define SIT9531X_PLL_REG_DEBUG 0xBD
+#define SIT9531X_PLL_DEBUG_UNLOCK 0xC3
+#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
+#define SIT9531X_DBG_DATA_BYTES 8
+
+/* Read code of the TDC phase tap, and the sign bit of its sample */
+#define SIT9531X_DBG_READ_CODE_TDC 69
+#define SIT9531X_TDC_SIGN_BIT 3
+#define SIT9531X_TDC_MAG_HI_MASK GENMASK(2, 0)
+
/*
* DIVN carried as fixed point, and the unit the DPLL ABI wants the
* fractional frequency offset in. Equal in value, distinct in meaning.
--
2.43.0
^ permalink raw reply [flat|nested] 28+ messages in thread* Re: [PATCH v10 11/14] dpll: sit9531x: add support to get phase offset on the connected input pin
2026-09-21 20:11 ` [PATCH v10 11/14] dpll: sit9531x: add support to get phase offset on the connected input pin Ali Rouhi
@ 2026-09-26 2:34 ` Jakub Kicinski
0 siblings, 0 replies; 28+ messages in thread
From: Jakub Kicinski @ 2026-09-26 2:34 UTC (permalink / raw)
To: arouhi
Cc: Jakub Kicinski, jiri, vadim.fedorenko, arkadiusz.kubalewski,
ivecera, robh, krzk+dt, conor+dt, cjubran, pabeni,
Oleg.Zadorozhnyi, prabhakar.mahadev-lad.rj, dev, devicetree,
netdev, linux-kernel
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
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 per-PLL debug window and converts the code to
picoseconds using the VCO frequency. Only the input the PLL has selected
gets a measurement; other pins and dormant PLLs report zero.
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index c8c3cd6a64baf..8f6f8ffc8dc13 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -2314,6 +2314,221 @@ int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
[ ... ]
> +/*
> + * sit9531x_phase_offset_read - read phase difference via TDC
> + * @phase_ps: output phase difference in picoseconds
> + *
> + * Reads the Time-to-Digital Converter (TDC) signed 35-bit code from the
> + * PLL page registers, then converts to picoseconds using the VCO
> + * frequency: phase_diff = tdc_code / fvco.
> + *
> + * Caller must hold sitdev->multiop_lock.
> + */
> +int sit9531x_phase_offset_read(struct sit9531x_dev *sitdev, u8 pll_idx,
> + s64 *phase_ps)
> +{
[Severity: Low]
This isn't a bug, but is "signed 35-bit code" accurate? The decode below
builds a 35-bit magnitude from DBG_DATA_4[2:0] through DBG_DATA_0. The sign
comes from a separate bit in DATA_4, so the field is 36-bit sign-magnitude:
sign = !!(v & BIT(SIT9531X_TDC_SIGN_BIT));
tdc_raw = (u64)(v & SIT9531X_TDC_MAG_HI_MASK) << 32;
regs.h also describes DATA_4 as "[34:32] + sign".
This header also opens with /* rather than /**. It doesn't document
@sitdev, @pll_idx or a Return: section, while
sit9531x_chan_selected_ref_read() just above it does.
[ ... ]
> + /*
> + * 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.
> + */
> + if (!sit9531x_read_pll_u8(sitdev, pll_idx,
> + SIT9531X_PLL_REG_DBG_WRITE_CODE,
> + &old_write_code) &&
> + !sit9531x_read_pll_u8(sitdev, pll_idx,
> + SIT9531X_PLL_REG_DBG_READ_CODE,
> + &old_read_code))
> + 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;
[Severity: Medium]
What happens if either snapshot read fails? have_old stays false, but the
sequence continues. SIT9531X_DBG_LOW_FREQ_CLK_BIT is still set in WRITE_CODE,
and SIT9531X_DBG_READ_CODE_TDC is still written to READ_CODE.
The restore at the relock label below is then skipped because have_old is
false. The PLL is left with the mux parked on the TDC and the slow sampling
clock selected, which is the state the comment above says must not be left
behind.
The next successful call then snapshots this modified state and restores to
it, so the change is never undone.
Separately, the two restore sit9531x_write_pll_u8() calls at relock ignore
their return values. A failed restore still lets the function return 0.
The v10 notes say "Restored the debug tap selection the read changes".
Should a failed snapshot abort before the tap is modified? Should restore
errors be folded into rc?
In this series, sit9531x_dbg_sample() reprograms both registers itself, so
no in-driver reader currently sees the wrong tap.
[ ... ]
> + 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);
> + goto relock;
> + }
[ ... ]
> +relock:
> + if (have_old) {
> + sit9531x_write_pll_u8(sitdev, pll_idx,
> + SIT9531X_PLL_REG_DBG_READ_CODE,
> + old_read_code);
> + sit9531x_write_pll_u8(sitdev, pll_idx,
> + SIT9531X_PLL_REG_DBG_WRITE_CODE,
> + old_write_code);
> + }
> +
[ ... ]
> + lock_rc = sit9531x_write_pll_u8(sitdev, pll_idx,
> + SIT9531X_PLL_REG_DEBUG,
> + SIT9531X_PLL_DEBUG_LOCK);
> + if (lock_rc && !rc)
> + rc = lock_rc;
> +
> + return rc;
> +}
[Severity: Medium]
On a dormant PLL, can a bus error still reach user space as a valid zero
reading?
sit9531x_get_fvco() runs only after the debug window has been unlocked, the
tap reprogrammed, the trigger read and DATA_4..DATA_0 read. When it returns
-ENODATA, the code jumps to relock with rc == -ENODATA.
The restore writes there ignore their errors. A failed lock write is also
dropped, because rc is already nonzero when this runs:
if (lock_rc && !rc)
rc = lock_rc;
The function returns -ENODATA. sit9531x_dpll_input_pin_phase_offset_get()
then turns that into success:
if (rc == -ENODATA) {
*phase_offset = 0;
return 0;
}
The PLL debug key can stay unlocked, and the tap may stay parked on the TDC,
while user space sees a successful 0.
sit9531x_dpll_selection_state_get() doesn't check chan->active. A dormant PLL
with a populated priority table can therefore pass the gate in the caller.
The v10 notes say "Gave the dormant-PLL case its own errno, so a bus error is
not reported as a valid zero reading."
Would it close this gap to check Fvco before opening the debug window, and
to fold lock and restore errors into rc whatever its previous value?
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index af1089f192b6f..5f7c2d01562c9 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -638,6 +638,123 @@ 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
> + *
[ ... ]
> + * Precondition, which this callback cannot create: the TDC compares
> + * against a signal the PLL drives, so a PLL driving no output with its
> + * zero-delay buffer off has nothing to measure. SiTime confirms this is
> + * a property of the hardware rather than of their measurement script.
> + * The script satisfies it by mapping a spare output and restarting the
> + * PLL -- side effects that do not belong in a getter, so a reading taken
> + * in that state is simply not meaningful.
[Severity: Medium]
If a reading in this state is not meaningful, what stops it from being
published?
Before sit9531x_phase_offset_read() runs, the only checks are the cached
CONNECTED state, the live ACTIVESEL match and the -ENODATA dormant-DIVN
case.
Take a PLL with a programmed DIVN and a selected reference but no routed or
enabled output. The TDC is still sampled. The value is scaled by
DPLL_PHASE_OFFSET_DIVIDER and returned as a valid DPLL_A_PIN_PHASE_OFFSET.
User space can't tell it apart from a real measurement.
The driver already tracks output routing in struct sit9531x_out (routed,
pll_idx, enabled). sit9531x_out_state_fetch() fills these from the OUT_MAP
registers and the Hi-Z force bits. From the code it isn't clear whether a
Hi-Z output counts as driving for the TDC, and ZDB state isn't tracked. The
no-routed-output case does look detectable, though.
Could the callback report 0 when the PLL drives no output, as it does for
the dormant-DIVN case?
The commit message doesn't mention this limitation, and it describes the
value as "the measurement".
[ ... ]
> + /*
> + * Which pin the sample belongs to is read from the device rather
> + * than taken from the monitor's cache: the device selects its own
> + * reference, so a cache up to a poll period old could attribute a
> + * live measurement to the pin that used to be selected.
> + */
> + rc = sit9531x_chan_selected_ref_read(sitdev, sitdpll->id,
> + &selected);
[ ... ]
> + sit9531x_dpll_selection_state_get(sitdev, sitdpll, dpin->id, &state);
> + if (state != DPLL_PIN_STATE_CONNECTED || selected != dpin->id) {
> + mutex_unlock(&sitdev->multiop_lock);
> + *phase_offset = 0;
> + return 0;
> + }
[Severity: Low]
Does this match the comment above it? The gate still needs
sit9531x_dpll_selection_state_get() to return CONNECTED, and that result
comes from the cached chan->selected_ref and chan->mode:
active_input = !chan->mode && chan->selected_ref == pin_id;
So the live ACTIVESEL read can only veto a sample. It doesn't decide which
pin gets it.
Suppose the device switches from IN0 to IN1 on its own. IN0 reports 0
because the live value no longer matches. IN1 also reports 0 because the
cache still says SELECTABLE. This lasts until the next
sit9531x_dev_periodic_work() poll refreshes the cache.
No wrong value is published. However, this comment and the
sit9531x_chan_selected_ref_read() kernel-doc ("close enough for reporting
pin state but not for attributing a measurement") both describe live-only
attribution.
Could the comments be updated to say that both the cached state and the live
read are required?
> +
> + rc = sit9531x_phase_offset_read(sitdev, sitdpll->id, &offset);
> + mutex_unlock(&sitdev->multiop_lock);
[Severity: Low]
Can the device fail over between the ACTIVESEL read above and the TDC latch?
Before the three SIT9531X_PLL_REG_DBG_TRIGGER reads latch the sample,
sit9531x_phase_offset_read() does several I2C transactions: the unlock, two
snapshot reads, the write-code update and the read-code write.
multiop_lock serialises the driver's accesses, but not the device's own
reference selection.
If the device switches reference in that window, the TDC samples against
the new reference. The result is then published as the old pin's phase
offset. This is the case the v10 change targets, now limited to a few
milliseconds.
Would re-reading ACTIVESEL after the latch, and reporting 0 on a mismatch,
close this?
[ ... ]
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH v10 10/14] dpll: sit9531x: add support to adjust output phase
2026-09-21 20:11 [PATCH v10 00/14] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (9 preceding siblings ...)
2026-09-21 20:11 ` [PATCH v10 11/14] dpll: sit9531x: add support to get phase offset on the connected input pin Ali Rouhi
@ 2026-09-21 20:11 ` Ali Rouhi
2026-09-26 2:34 ` Jakub Kicinski
2026-09-21 20:11 ` [PATCH v10 12/14] dpll: sit9531x: add support to get fractional frequency offset Ali Rouhi
` (2 subsequent siblings)
13 siblings, 1 reply; 28+ messages in thread
From: Ali Rouhi @ 2026-09-21 20:11 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi,
prabhakar.mahadev-lad.rj, dev, devicetree, netdev, linux-kernel,
Ali Rouhi
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Shift an output in time against the others driven by the same PLL. The
device has a coarse delay counted in VCO cycles and a three-bit fine field
in fixed thirty-picosecond steps, so a requested offset is split between
the two and what the core reads back is what the registers hold rather
than what was asked for.
Delay only ever advances, so an offset larger than one output period is
folded back into a single period -- for a periodic signal that is the same
phase. The write takes effect in the programming state, which is left
with the loops re-locked even when a write inside it failed.
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>
---
Notes:
Changes in v10:
Read the programmed delay back from the device: decoded at probe, so a
delay the profile carries can be cleared, and re-read when a request
could not be confirmed.
A rate change whose re-timing failed is reported as the rate change it
was, with the re-timing warned about.
Said in the changelog that the device has no per-output phase flush, so
realigning one output restarts the divider phase of all of them.
Folded the three-way sign handling into one abs() and a remainder, and
skipped the coarse and fine encoding for a zero offset, both as Ivan
Vecera suggested.
drivers/dpll/sit9531x/core.c | 341 ++++++++++++++++++++++++++++++++++-
drivers/dpll/sit9531x/core.h | 14 ++
drivers/dpll/sit9531x/dpll.c | 78 ++++++++
drivers/dpll/sit9531x/prop.c | 19 ++
drivers/dpll/sit9531x/regs.h | 28 +++
5 files changed, 477 insertions(+), 3 deletions(-)
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 8daf0fbf6772..c8c3cd6a64ba 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -1811,6 +1811,69 @@ static int sit9531x_output_divo_write(struct sit9531x_dev *sitdev, u8 out_idx,
return rc;
}
+/**
+ * sit9531x_output_phase_read - read an output's programmed delay back
+ * @sitdev: device pointer
+ * @out_idx: logical output index
+ * @phase_ps: result in picoseconds, always a delay (never an advance)
+ *
+ * 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, so a request that was
+ * made as an advance reads back as the equivalent delay.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, <0 on 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;
+ int rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (out_idx >= info->num_outputs)
+ return -EINVAL;
+
+ rc = sit9531x_get_fvco(sitdev, sitdev->out[out_idx].pll_idx, &fvco);
+ if (rc)
+ return rc == -ENODATA ? -ENODEV : 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];
+
+ ps = mul_u64_u64_div_u64(coarse, 1000000000000ULL, fvco);
+ ps += (u64)fine * SIT9531X_OUT_PRG_FINE_STEP_PS;
+
+ *phase_ps = (s32)min_t(u64, ps, S32_MAX);
+
+ return 0;
+}
+
int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
u8 pll_idx, u64 frequency)
{
@@ -1864,7 +1927,41 @@ int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
sitdev->out[out_idx].freq = div64_u64(fvco, divo);
- return 0;
+ /*
+ * The programmed reset delay counts VCO cycles against the output
+ * period in force when it was written, so a rate change silently
+ * re-times a previously requested phase adjust. Re-encode the
+ * cached picosecond request against the new rate.
+ *
+ * Keyed off whether a delay was ever programmed rather than off the
+ * cached value: quantization can leave a whole period in the
+ * registers, which is the same phase and caches as zero, and that
+ * still has to be re-timed when the period changes.
+ */
+ 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;
}
/*
@@ -1946,14 +2043,227 @@ 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.
*/
+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;
+ s64 phase_norm_ps = 0;
+ u8 page, base, prog6_val, fine = 0;
+ u8 old_bytes[5], new_bytes[5], i;
+ u8 pll_idx, slot;
+ u64 freq;
+ int rc, ret, rb_rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (out_idx >= info->num_outputs)
+ return -EINVAL;
+
+ pll_idx = sitdev->out[out_idx].pll_idx;
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+
+ freq = sitdev->out[out_idx].freq;
+ if (!freq) {
+ /*
+ * The cache is only seeded by a DT frequency list or an
+ * earlier get/set; a board without supported-frequencies-hz
+ * would otherwise get -EINVAL on every phase request forever.
+ * Read the effective rate back from the divider chain.
+ */
+ rc = sit9531x_output_freq_get(sitdev, out_idx, &freq);
+ if (rc)
+ return rc;
+ if (!freq)
+ return -EINVAL;
+ }
+
+ rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
+ if (rc)
+ return rc == -ENODATA ? -ENODEV : rc;
+
+ t_out_ps = div64_u64(1000000000000ULL, freq);
+ if (!t_out_ps)
+ return -EINVAL;
+
+ /*
+ * Convert to unsigned absolute delay. Both signs are folded
+ * modulo one period: positive delays wrap naturally, negative
+ * delays are rendered as T_out - |phase|. 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;
+
+ /*
+ * coarse_cycles = abs_ps * Fvco / 1e12 ps/s.
+ * mul_u64_u64_div_u64() avoids overflow when abs_ps approaches
+ * one second of 1 PPS wrap-around.
+ */
+ coarse = mul_u64_u64_div_u64(abs_ps, fvco, 1000000000000ULL);
+ if (coarse >= (1ULL << SIT9531X_OUT_PRG_COARSE_BITS))
+ return -ERANGE;
+
+ /*
+ * Fine delay = round((abs_ps - coarse * vco_period_ps) / 30 ps)
+ */
+ coarse_ps = mul_u64_u64_div_u64(coarse, 1000000000000ULL, fvco);
+ rem_ps = (abs_ps > coarse_ps) ? (abs_ps - coarse_ps) : 0;
+ if (rem_ps) {
+ u64 steps;
+
+ steps = div64_u64(rem_ps +
+ SIT9531X_OUT_PRG_FINE_STEP_PS / 2,
+ SIT9531X_OUT_PRG_FINE_STEP_PS);
+ if (steps > SIT9531X_OUT_PRG_FINE_MAX)
+ steps = SIT9531X_OUT_PRG_FINE_MAX;
+ fine = (u8)steps;
+ }
+ }
+
+ /*
+ * Map logical output index to the chip's physical output slot.
+ * On SiT95317 the eight logical outputs land on chip slots
+ * {0, 3, 4, 5, 7, 8, 9, 11}; on SiT95316 the map is identity.
+ * Page/base must address the slot, not the logical index.
+ */
+ slot = info->clkout_map[out_idx];
+ page = (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX) ?
+ SIT9531X_PAGE_OUTSYS1 : SIT9531X_PAGE_OUTSYS0;
+ base = SIT9531X_OUT_PRG_DELAY_BASE +
+ SIT9531X_OUT_PRG_SLOT_STRIDE * (slot % 6);
+
+ /*
+ * The PRG_RST_DELAY bytes live in the output system, so the writes
+ * only take effect when made inside the PRG_CMD programming state and
+ * committed to the NVM shadow, exactly like sit9531x_output_freq_set().
+ */
+ rc = sit9531x_prg_enter(sitdev);
+ if (rc)
+ return rc;
+
+ for (i = 0; i < ARRAY_SIZE(old_bytes); i++) {
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG(page, base + i),
+ &old_bytes[i]);
+ if (rc)
+ goto commit;
+ }
+
+ /* PROG6 RMW: preserve OPSTG_VCASC_BUMP in [7:5] */
+ prog6_val = old_bytes[0] & SIT9531X_OUT_PRG_OPSTG_MASK;
+ prog6_val |= (fine << SIT9531X_OUT_PRG_FINE_SHIFT) &
+ SIT9531X_OUT_PRG_FINE_MASK;
+ prog6_val |= (u8)((coarse >> 32) & SIT9531X_OUT_PRG_COARSE_HI_MASK);
+
+ new_bytes[0] = prog6_val;
+ new_bytes[1] = (u8)((coarse >> 24) & 0xFF);
+ new_bytes[2] = (u8)((coarse >> 16) & 0xFF);
+ new_bytes[3] = (u8)((coarse >> 8) & 0xFF);
+ new_bytes[4] = (u8)(coarse & 0xFF);
+
+ for (i = 0; i < ARRAY_SIZE(new_bytes); i++) {
+ rc = sit9531x_write_u8(sitdev,
+ SIT9531X_REG(page, base + i),
+ new_bytes[i]);
+ if (rc)
+ goto rollback;
+ }
+
+ goto commit;
+
+rollback:
+ rb_rc = 0;
+ for (i = 0; i < ARRAY_SIZE(old_bytes); i++) {
+ ret = sit9531x_write_u8(sitdev,
+ SIT9531X_REG(page, base + i),
+ old_bytes[i]);
+ if (ret && !rb_rc)
+ rb_rc = ret;
+ }
+ if (rb_rc) {
+ dev_err(sitdev->dev,
+ "out%u: phase-adjust rollback failed (%d), the delay registers are part old and part new\n",
+ out_idx, rb_rc);
+ if (!rc)
+ rc = rb_rc;
+ }
+
+commit:
+ /*
+ * Always leave the PRG_CMD state via prg_commit(), even on a
+ * mid-sequence write failure, so the output loops are re-locked rather
+ * than stranded unlocked; keep the first error.
+ */
+ ret = sit9531x_prg_commit(sitdev);
+ if (ret && !rc)
+ rc = ret;
+ if (rc)
+ return rc;
+
+ /*
+ * Restart the output divider phase so the freshly programmed delay is
+ * applied against a known edge instead of the divider's arbitrary
+ * running phase.
+ */
+ rc = sit9531x_output_phase_flush(sitdev, pll_idx);
+ if (rc)
+ return rc;
+
+ /*
+ * Cache what the registers realize, and only once every step has
+ * succeeded: the core drops a repeated request with the same value,
+ * so a cache updated by a failed call would make the retry a no-op.
+ *
+ * Quantizing to whole VCO cycles plus 30 ps steps can land a few
+ * picoseconds past the end of the period, which would wrap the
+ * subtraction below; one period is the most a delay can be.
+ */
+ coarse_ps = mul_u64_u64_div_u64(coarse, 1000000000000ULL, fvco);
+ abs_ps = coarse_ps + (u64)fine * SIT9531X_OUT_PRG_FINE_STEP_PS;
+ if (abs_ps > t_out_ps)
+ abs_ps = t_out_ps;
+ if (phase_norm_ps < 0)
+ sitdev->out[out_idx].phase_adj =
+ abs_ps ? -(s32)(t_out_ps - abs_ps) : 0;
+ else
+ /*
+ * The cache is an s32 because that is what the ABI carries.
+ * A delay is bounded by the output period, which on a slow
+ * output is wider than that, so bound the cast. The negative
+ * branch above needs no bound: what it stores is the advance
+ * that was asked for, and that came in as an s32.
+ */
+ sitdev->out[out_idx].phase_adj = (s32)min(abs_ps,
+ (u64)S32_MAX);
+
+ /*
+ * Record that a delay is programmed whatever it quantized to. A
+ * request that lands on a whole period caches as zero, and the rate
+ * change that follows still has to re-time what the registers hold.
+ */
+ sitdev->out[out_idx].phase_armed = true;
+
+ return 0;
+}
+
/*
* sit9531x_clear_notifications - clear all notification registers
*
@@ -2368,12 +2678,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 != -ENODEV) {
+ 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 3b35bf1c4cd5..7848ac9bd6ca 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -102,6 +102,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
* @label: board label from DT or default
*/
struct sit9531x_out {
@@ -111,6 +118,9 @@ struct sit9531x_out {
bool state_stale;
bool routed;
u8 pll_idx;
+ s32 phase_adj;
+ bool phase_armed;
+ bool phase_stale;
const char *label;
};
@@ -268,6 +278,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 c1ab202bf297..af1089f192b6 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -868,12 +868,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 8270b8ee91be..82e8cd2266f5 100644
--- a/drivers/dpll/sit9531x/prop.c
+++ b/drivers/dpll/sit9531x/prop.c
@@ -228,6 +228,25 @@ 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.
+ */
+ /* +/-1 ms, in ps */
+ props->dpll_props.phase_range.min = -1000000000;
+ props->dpll_props.phase_range.max = 1000000000;
+ /*
+ * 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 8ce048e9c8f1..cbce62404c97 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -194,6 +194,34 @@
#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] */
+#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] 28+ messages in thread* Re: [PATCH v10 10/14] dpll: sit9531x: add support to adjust output phase
2026-09-21 20:11 ` [PATCH v10 10/14] dpll: sit9531x: add support to adjust output phase Ali Rouhi
@ 2026-09-26 2:34 ` Jakub Kicinski
0 siblings, 0 replies; 28+ messages in thread
From: Jakub Kicinski @ 2026-09-26 2:34 UTC (permalink / raw)
To: arouhi
Cc: Jakub Kicinski, jiri, vadim.fedorenko, arkadiusz.kubalewski,
ivecera, robh, krzk+dt, conor+dt, cjubran, pabeni,
Oleg.Zadorozhnyi, prabhakar.mahadev-lad.rj, dev, devicetree,
netdev, linux-kernel
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
dpll: sit9531x: add support to adjust output phase
This adds phase_adjust_get/set ops for sit9531x output pins. A requested
offset is split into a coarse delay counted in VCO cycles and a fine delay
in 30 ps steps, written to the per-output PRG_RST_DELAY registers. The
programmed delay is read back at probe to seed the cache, and a rate change
re-encodes an armed phase adjust against the new output period.
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index 8daf0fbf67720..c8c3cd6a64baf 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -1811,6 +1811,69 @@ static int sit9531x_output_divo_write(struct sit9531x_dev *sitdev, u8 out_idx,
> return rc;
> }
>
> +/**
> + * sit9531x_output_phase_read - read an output's programmed delay back
[ ... ]
> + * doing. The registers carry an unsigned delay, so a request that was
> + * made as an advance reads back as the equivalent delay.
[ ... ]
> + ps = mul_u64_u64_div_u64(coarse, 1000000000000ULL, fvco);
> + ps += (u64)fine * SIT9531X_OUT_PRG_FINE_STEP_PS;
> +
> + *phase_ps = (s32)min_t(u64, ps, S32_MAX);
[Severity: Medium]
Can this saturation report a value that is neither the real delay nor an
equivalent phase?
The decoded delay is never folded against the output period, but
sit9531x_output_phase_adjust_set() encodes an advance as T_out - |advance|.
Take a 1 Hz output: a -1000 ps advance is held as about 999999999000 ps and
reads back here as 2147483647. The same happens on any output whose period
is above about 2.147 ms (1 PPS, 100 Hz), and for any profile delay above
2.147 ms.
That doesn't match the kernel-doc above ("reads back as the equivalent
delay"), or the @phase_adj description in core.h.
Both sit9531x_dev_state_fetch() and the stale read-back in
sit9531x_dpll_output_pin_phase_adjust_get() store this value in phase_adj
and set phase_armed. The getter then reports a value outside the
advertised phase_range of +/-1000000000 ps. If userspace writes that value
back, dpll_pin_phase_adj_set() rejects it.
After that, a rate change does this:
sit9531x_output_freq_set()
sit9531x_output_phase_adjust_set(sitdev, out_idx, S32_MAX)
That call skips the core range check the abs() comment relies on. It
programs about 2.147 ms folded into the new period, runs a PLL-wide flush,
and the rate change is still reported as successful.
> int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
> u8 pll_idx, u64 frequency)
> {
> @@ -1864,7 +1927,41 @@ int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
>
> sitdev->out[out_idx].freq = div64_u64(fvco, divo);
>
> - return 0;
> + /*
> + * The programmed reset delay counts VCO cycles against the output
> + * period in force when it was written, so a rate change silently
> + * re-times a previously requested phase adjust. Re-encode the
> + * cached picosecond request against the new rate.
> + *
> + * Keyed off whether a delay was ever programmed rather than off the
> + * cached value: quantization can leave a whole period in the
> + * registers, which is the same phase and caches as zero, and that
> + * still has to be re-timed when the period changes.
> + */
> + if (sitdev->out[out_idx].phase_armed) {
> + 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: Low]
Do non-negative delays need this re-encoding?
The encoder computes coarse = abs_ps * Fvco / 1e12 and takes fine from the
remainder. sit9531x_output_freq_set() changes only DIVO, not Fvco, so a
positive delay keeps its absolute timing across a rate change. Only an
advance, encoded as T_out - |x|, depends on the period.
For positive delays, every rate change now adds a second
prg_enter()/prg_commit() cycle, with its 100 ms msleep. It also adds a
second sit9531x_output_phase_flush(), which restarts the divider phase of
every output on the PLL. This includes delays seeded from the factory
profile through phase_armed = !!phase_ps in sit9531x_dev_state_fetch().
The commit message body doesn't mention this change to frequency_set
either. It only shows up in the v10 notes.
[ ... ]
> @@ -1946,14 +2043,227 @@ int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
[ ... ]
> +int sit9531x_output_phase_adjust_set(struct sit9531x_dev *sitdev,
> + u8 out_idx, s32 phase_ps)
> +{
[ ... ]
> + t_out_ps = div64_u64(1000000000000ULL, freq);
> + if (!t_out_ps)
> + return -EINVAL;
[Severity: Low]
Can the wrap period be far off for a low-rate output that runs at a
non-integer frequency?
freq comes from the cache or from sit9531x_output_freq_get(), which stores
div64_u64(fvco, divo) truncated to whole Hz. With Fvco 5 GHz and DIVO 4e9
the output runs at 1.25 Hz (period 8e11 ps). freq is 1, though, so t_out_ps
becomes 1e12.
A -1000 ps request is then programmed as 999999999000 ps. Against the real
8e11 ps period that is roughly a 0.2 s delay, not a 1 ns advance, while the
cache reports -1000.
Rates the driver programs itself are close enough that the error is
negligible. This only affects low-rate, non-integer-Hz outputs that come
from a profile.
[ ... ]
> + 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;
> + }
[Severity: Low]
Should this skip the commit and flush when new_bytes[] matches
old_bytes[]?
The pin advertises phase_gran = 1 but caches the quantized value. So the
core's no-op check in dpll_pin_phase_adj_set() rarely matches when an
off-grid request is repeated:
if (phase_adj == old_phase_adj)
return 0;
Each repeat of the same request goes through the whole sequence again:
prg_enter(), a rewrite of the same five bytes, prg_commit() with its
100 ms settle, and sit9531x_output_phase_flush(). That flush restarts the
divider phase of every output on the PLL, which the commit message calls a
visible edge jump on the other outputs.
[ ... ]
> +commit:
> + /*
> + * Always leave the PRG_CMD state via prg_commit(), even on a
> + * mid-sequence write failure, so the output loops are re-locked rather
> + * than stranded unlocked; keep the first error.
> + */
> + ret = sit9531x_prg_commit(sitdev);
> + if (ret && !rc)
> + rc = ret;
> + if (rc)
> + return rc;
> +
> + /*
> + * Restart the output divider phase so the freshly programmed delay is
> + * applied against a known edge instead of the divider's arbitrary
> + * running phase.
> + */
> + rc = sit9531x_output_phase_flush(sitdev, pll_idx);
> + if (rc)
> + return rc;
[Severity: Medium]
Should these error returns mark the cached phase as unconfirmed?
Three paths return an error when the new PRG_RST_DELAY bytes may already
be on the device:
- all five writes succeed, then sit9531x_prg_commit() fails. The failure
can be in UPDATE_NVM, LOOP_LOCK or the debug-lock write, so it can come
after the delay has latched.
- the commit succeeds and sit9531x_output_phase_flush() fails.
- a write fails and the rollback also fails.
None of these paths touch phase_adj, phase_armed or phase_stale.
sit9531x_dpll_output_pin_phase_adjust_set() doesn't set phase_stale
either. The only place phase_stale is set is the re-timing branch in
sit9531x_output_freq_set().
The getter's comment says its read-back covers exactly this case:
* A request whose writes reached the device but whose commit or
* phase flush failed left the cache describing the delay before it.
On the direct request path, though, the read-back never runs. The getter
keeps reporting the old value Y while the output runs the new delay X.
dpll_pin_phase_adj_set() returns 0 when a request equals what the getter
reports. So a request to go back to Y, such as 0 to clear the delay, is
silently dropped.
If this was the first request on the output, phase_armed also stays
false. A later rate change then skips re-timing a delay the registers
really hold.
> +
> + /*
> + * Cache what the registers realize, and only once every step has
> + * succeeded: the core drops a repeated request with the same value,
> + * so a cache updated by a failed call would make the retry a no-op.
> + *
> + * Quantizing to whole VCO cycles plus 30 ps steps can land a few
> + * picoseconds past the end of the period, which would wrap the
> + * subtraction below; one period is the most a delay can be.
> + */
> + coarse_ps = mul_u64_u64_div_u64(coarse, 1000000000000ULL, fvco);
> + abs_ps = coarse_ps + (u64)fine * SIT9531X_OUT_PRG_FINE_STEP_PS;
> + if (abs_ps > t_out_ps)
> + abs_ps = t_out_ps;
> + if (phase_norm_ps < 0)
> + sitdev->out[out_idx].phase_adj =
> + abs_ps ? -(s32)(t_out_ps - abs_ps) : 0;
[Severity: Low]
Does this clamp hide a residual the registers still hold?
Only the cached value is clamped. The registers that were just committed
and flushed still hold a delay that runs past the period.
Take Fvco 5 GHz, a 10 MHz output (T = 100000 ps) and a -1 ps request:
abs_ps = 99999
coarse = 499 (99800 ps), remainder 199 ps, fine = 7 (210 ps)
total = 100010 ps, clamped to 100000
phase_adj = -(s32)(100000 - 100000) = 0
The output runs with a +10 ps residual while the getter reports 0. A later
request for 0 is dropped by dpll_pin_phase_adj_set() because it equals the
cached value, so the residual can't be cleared that way.
> + else
[ ... ]
> + sitdev->out[out_idx].phase_adj = (s32)min(abs_ps,
> + (u64)S32_MAX);
> +
> + /*
> + * Record that a delay is programmed whatever it quantized to. A
> + * request that lands on a whole period caches as zero, and the rate
> + * change that follows still has to re-time what the registers hold.
> + */
> + sitdev->out[out_idx].phase_armed = true;
[Severity: Low]
This isn't a bug, but is "caches as zero" accurate for positive requests?
The re-timing comment in sit9531x_output_freq_set() makes the same claim.
Only the negative branch stores 0. The positive branch stores
min(abs_ps, S32_MAX), and abs_ps has already been clamped to t_out_ps, so a
whole-period result caches as t_out_ps.
For example, Fvco 5 GHz, a 10 MHz output (T = 100000 ps) and a +99995 ps
request give coarse 499 (99800 ps) plus fine 7 (210 ps). That is
100010 ps, which is clamped to 100000, and the cache holds 100000.
After a re-probe, sit9531x_output_phase_read() reports the same registers
as 100010, because it applies no period clamp.
[Severity: Low]
Should phase_armed be set for a 0 ps request?
For 0 ps the if (abs_ps) block is skipped, so coarse and fine stay 0 and no
delay is programmed. phase_armed is still set to true here. By contrast,
sit9531x_dev_state_fetch() and the getter's read-back both use !!phase_ps.
Nothing ever clears phase_armed.
So after a delay is cleared with 0, every later sit9531x_output_freq_set()
on that output still runs the re-timing block. That means another
prg_enter()/prg_commit() with the 100 ms settle and another PLL-wide
sit9531x_output_phase_flush(), just to write zeros again.
The whole-period case the flag is meant for has abs_ps != 0 at this point,
so the two cases can be told apart.
[ ... ]
> @@ -2368,12 +2678,37 @@ static int sit9531x_dev_state_fetch(struct sit9531x_dev *sitdev)
[ ... ]
> + /*
> + * 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 != -ENODEV) {
[Severity: Low]
Does -ENODEV actually identify an unrouted output here?
For an output that no PLL maps, sit9531x_out_state_fetch() does this:
/* Output not mapped to any PLL */
out->pll_idx = 0;
out->routed = false;
sit9531x_output_phase_read() checks neither routed nor pll_idx. It calls
sit9531x_get_fvco() for PLLA, which normally succeeds with the DT override
or PLLA's DIVN-derived rate. -ENODEV only comes back when PLLA's DIVN is
unprogrammed.
As a result:
- the delay registers of every unrouted slot are read at probe.
- a read error on any of them fails the whole probe.
- the value is decoded against the wrong PLL's VCO and stored in
phase_adj/phase_armed.
Unrouted outputs have no DPLL pin, so these values are never exposed, but
the comment doesn't match what the code does. Also,
sit9531x_output_freq_get() returns -ENODEV for a similar condition, while
sit9531x_output_phase_adjust_set() returns -EINVAL.
[ ... ]
> diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
> index 8270b8ee91bec..82e8cd2266f5a 100644
> --- a/drivers/dpll/sit9531x/prop.c
> +++ b/drivers/dpll/sit9531x/prop.c
> @@ -228,6 +228,25 @@ sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
[ ... ]
> + /*
> + * The fine step is 30 ps, but requests are accepted at 1 ps
> + * resolution and rounded to the nearest achievable delay, so
> + * advertise the request granularity, not the hardware step.
> + */
> + props->dpll_props.phase_gran = 1;
[Severity: Low]
Is "rounded to the nearest achievable delay" what the encoder does?
sit9531x_output_phase_adjust_set() floors the coarse count and only rounds
the remainder into 30 ps fine steps. It never tries coarse + 1:
coarse = mul_u64_u64_div_u64(abs_ps, fvco, 1000000000000ULL);
Take Fvco = SIT9531X_FVCO_HIGHBAND_MAX (7.8125 GHz, a 128 ps VCO period)
and a 127 ps request. The encoder picks coarse 0 and fine 4, which is
120 ps (7 ps off). Coarse 1 with fine 0 would give 128 ps (1 ps off).
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH v10 12/14] dpll: sit9531x: add support to get fractional frequency offset
2026-09-21 20:11 [PATCH v10 00/14] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (10 preceding siblings ...)
2026-09-21 20:11 ` [PATCH v10 10/14] dpll: sit9531x: add support to adjust output phase Ali Rouhi
@ 2026-09-21 20:11 ` Ali Rouhi
2026-09-26 2:34 ` Jakub Kicinski
2026-09-21 20:11 ` [PATCH v10 13/14] dpll: sit9531x: model the inter-PLL sync net as a pair of pins Ali Rouhi
2026-09-21 20:11 ` [PATCH v10 14/14] dpll: sit9531x: allow the device tree to override two board facts Ali Rouhi
13 siblings, 1 reply; 28+ messages in thread
From: Ali Rouhi @ 2026-09-21 20:11 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi,
prabhakar.mahadev-lad.rj, dev, devicetree, netdev, linux-kernel,
Ali Rouhi
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Report how far a PLL's reference is from nominal, as a fraction of the
nominal rate.
The device does not measure that directly, but it does run a feedback
divider that the loop adjusts to keep the reference in step: the
difference between the divider the loop is running and the divider the
configuration asked for is exactly the offset of the reference. Both are
read from the chip -- the configured one from its registers, the running
one through the debug window -- and the offset falls out of the ratio.
The running value comes from the same latched debug window as the phase
offset, so it is triggered three times per sample for the same reason.
The running divider is two numbers read through a window that exposes one
tap at a time, so the numerator and the denominator come from separate
samples and a loop moving between them can tear the pair. On a locked
loop the divider moves by parts per trillion between two register reads,
which is below what this measurement resolves; the part offers no way to
latch both at once.
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
Notes:
Changes in v10:
Guarded the runtime DIVN tap the same way as the configured one, and
gave the denominator's high bit its own name rather than borrowing the
integer field's.
Completed the kernel-doc and the documented return contract.
drivers/dpll/sit9531x/core.c | 217 +++++++++++++++++++++++++++++++++++
drivers/dpll/sit9531x/core.h | 1 +
drivers/dpll/sit9531x/dpll.c | 51 ++++++++
drivers/dpll/sit9531x/regs.h | 14 +++
4 files changed, 283 insertions(+)
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 8f6f8ffc8dc1..1cb691f04019 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -1396,6 +1396,93 @@ static int sit9531x_is_xo_doubler_enabled(struct sit9531x_dev *sitdev)
return (~val >> SIT9531X_XO_DOUBLER_ENB_BIT) & 1u;
}
+/*
+ * sit9531x_dbg_sample - latch and read a signal pathway debug sample
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ * @read_code: which tap of the pathway to sample
+ * @low_freq_clk: sample with the slow debug clock, which taps below
+ * 200 kHz need
+ * @buf: result, least significant byte first
+ * @len: bytes to read, at most SIT9531X_DBG_DATA_BYTES
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int sit9531x_dbg_sample(struct sit9531x_dev *sitdev, u8 pll_idx,
+ u8 read_code, bool low_freq_clk,
+ u8 *buf, unsigned int len)
+{
+ unsigned int i;
+ int rc, lock_rc;
+ u8 v;
+
+ if (len > SIT9531X_DBG_DATA_BYTES)
+ return -EINVAL;
+
+ rc = sit9531x_write_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_DEBUG,
+ SIT9531X_PLL_DEBUG_UNLOCK);
+ if (rc)
+ goto relock;
+
+ /*
+ * Select the debug clock this tap needs. The device never clears
+ * the bit, so a read that left it to whatever the previous one set
+ * would depend on the order the taps happened to be read in. Taps
+ * below 200 kHz need the slow clock; the divider taps do not.
+ */
+ rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_WRITE_CODE,
+ SIT9531X_DBG_LOW_FREQ_CLK_BIT,
+ low_freq_clk ?
+ SIT9531X_DBG_LOW_FREQ_CLK_BIT : 0);
+ if (rc)
+ goto relock;
+
+ rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_READ_CODE, read_code);
+ if (rc)
+ goto relock;
+
+ /*
+ * Reading the trigger latches a sample of the selected tap. Read it
+ * three times, as the documented phase-difference procedure does and as
+ * sit9531x_phase_offset_read() already did: a single read returns
+ * the previous latch, so a caller sampling repeatedly gets the same
+ * value back however much the tap has moved.
+ */
+ for (i = 0; i < SIT9531X_DBG_LATCH_READS; i++) {
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_TRIGGER, &v);
+ if (rc)
+ goto relock;
+ }
+
+ for (i = 0; i < len; i++) {
+ rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DBG_DATA_0 + i,
+ &buf[i]);
+ if (rc)
+ goto relock;
+ }
+
+ rc = 0;
+
+relock:
+ /*
+ * Close the debug window again. The key register opens every debug
+ * register on this PLL while it holds the unlock value, and these
+ * samples run on ordinary monitoring paths, so leaving it open would
+ * unlock the block for as long as the device runs.
+ */
+ lock_rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+ SIT9531X_PLL_REG_DEBUG,
+ SIT9531X_PLL_DEBUG_LOCK);
+ if (lock_rc && !rc)
+ rc = lock_rc;
+
+ return rc;
+}
+
/*
* 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
@@ -1477,6 +1564,136 @@ static int sit9531x_divn_static(struct sit9531x_dev *sitdev, u8 pll_idx,
return 0;
}
+/*
+ * sit9531x_divn_runtime - read the DIVN the digital loop is commanding
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ * @divn: result, fixed point as per sit9531x_divn_fixed()
+ *
+ * Same quantity as sit9531x_divn_static(), but sampled from the running
+ * loop rather than from the configuration registers, and carried at a
+ * wider precision: the numerator is 48 bits, two's complement, the
+ * denominator 49. The integer part shares its tap with the numerator.
+ *
+ * The denominator is taken as it reads. The configuration register holds
+ * the divisor minus one, and correcting for that reproduces a profile's
+ * stated VCO exactly, to the last bit of the fraction; this tap is a wider
+ * field sampled from the loop itself and the documented readback uses it as it
+ * comes, so the bias is not applied here.
+ *
+ * The numerator and the denominator come from two separate latches:
+ * the debug window exposes one tap at a time, so the pair can be torn
+ * by a loop that moves between them. The divider changes by parts per
+ * trillion between samples on a locked loop, which is below what this
+ * measurement resolves.
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int sit9531x_divn_runtime(struct sit9531x_dev *sitdev, u8 pll_idx,
+ s64 *divn)
+{
+ u8 buf[SIT9531X_DBG_DATA_BYTES];
+ u64 fracn_raw = 0, fracd = 0;
+ u32 int_part;
+ s64 fracn;
+ int rc, i;
+
+ rc = sit9531x_dbg_sample(sitdev, pll_idx, SIT9531X_DBG_READ_CODE_DIVN,
+ false, buf, SIT9531X_DBG_DATA_BYTES);
+ if (rc)
+ return rc;
+
+ for (i = 5; i >= 0; i--)
+ fracn_raw = (fracn_raw << 8) | buf[i];
+
+ int_part = buf[6] | ((u32)(buf[7] & SIT9531X_DIVN_RT_INT_HI_BIT) << 8);
+
+ rc = sit9531x_dbg_sample(sitdev, pll_idx,
+ SIT9531X_DBG_READ_CODE_DIVN_DEN, false, buf,
+ SIT9531X_DBG_DATA_BYTES);
+ if (rc)
+ return rc;
+
+ for (i = 5; i >= 0; i--)
+ fracd = (fracd << 8) | buf[i];
+
+ fracd |= (u64)(buf[6] & SIT9531X_DIVN_RT_DEN_HI_BIT) << 48;
+
+ fracn = sign_extend64(fracn_raw, SIT9531X_DIVN_RT_NUM_BITS - 1);
+
+ /*
+ * sit9531x_divn_fixed() drops the fractional part when the
+ * denominator is zero. That is right for a configuration register
+ * that was never programmed, but a running loop reading zero means
+ * the sample is unusable, and returning the whole part alone would
+ * put a wrong DIVN into the frequency offset without saying so.
+ *
+ * A numerator at or above the denominator is not a fraction either,
+ * and would divide by a denominator small enough for the quotient
+ * to leave u64 -- a divide-error exception on x86 rather than a
+ * value sit9531x_get_fvco() could reject.
+ */
+ if (!fracd || (u64)abs(fracn) >= fracd)
+ return -ENODATA;
+ *divn = sit9531x_divn_fixed(int_part, fracn, fracd);
+
+ return 0;
+}
+
+/**
+ * sit9531x_pll_ffo_ppt - fractional frequency offset of a PLL's reference
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ * @ffo: result in parts per trillion
+ *
+ * A locked PLL commands whatever DIVN keeps its VCO tracking the
+ * reference. How far that sits from the configured DIVN is how far the
+ * reference sits from the local oscillator, which is the fractional
+ * frequency offset the DPLL ABI reports for the pin feeding the device.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, <0 on error. -ENODATA covers every reason the
+ * ratio cannot be formed: a DIVN that was never programmed, a runtime
+ * sample that is not a running loop, and a configured divider below one
+ * whole unit.
+ */
+int sit9531x_pll_ffo_ppt(struct sit9531x_dev *sitdev, u8 pll_idx, s64 *ffo)
+{
+ s64 configured, running, delta;
+ u64 magnitude;
+ int rc;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+
+ rc = sit9531x_divn_static(sitdev, pll_idx, &configured);
+ if (rc)
+ return rc;
+ /*
+ * Scaling by a divider far below one whole cycle overflows the
+ * 64-bit product and comes back as ~0, which would be reported
+ * as a plausible offset. A DIVN that small is not a running
+ * loop, so treat it as no measurement.
+ */
+ if (configured < SIT9531X_DIVN_SCALE)
+ return -ENODATA;
+
+ rc = sit9531x_divn_runtime(sitdev, pll_idx, &running);
+ if (rc)
+ return rc;
+
+ delta = running - configured;
+ magnitude = mul_u64_u64_div_u64(abs(delta), SIT9531X_PPT_PER_UNIT,
+ (u64)configured);
+
+ *ffo = delta < 0 ? -(s64)magnitude : (s64)magnitude;
+
+ return 0;
+}
+
/*
* sit9531x_get_fvco - read VCO frequency from chip's DIVN registers
*
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index f2750c319ff3..1d77b89e83ec 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -289,6 +289,7 @@ int sit9531x_clear_notifications(struct sit9531x_dev *sitdev);
/* ---- INTSYNC (inter-PLL synchronization) ---- */
/* ---- Phase offset (TDC readback) ---- */
+int sit9531x_pll_ffo_ppt(struct sit9531x_dev *sitdev, u8 pll_idx, s64 *ffo);
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,
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index 5f7c2d01562c..fcdd19e0bd3b 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -638,6 +638,50 @@ sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
return 0;
}
+/*
+ * sit9531x_dpll_input_pin_ffo_get - read the input's frequency offset
+ *
+ * The offset is derived from how far the PLL's running DIVN sits from
+ * its configured one, which only says something about the reference the
+ * PLL is actually tracking. For every other input there is no
+ * measurement, and -ENODATA leaves the attribute out rather than
+ * reporting the active reference's figure against the wrong pin.
+ */
+static int
+sit9531x_dpll_input_pin_ffo_get(const struct dpll_pin *pin, void *pin_priv,
+ const struct dpll_device *dpll, void *dpll_priv,
+ struct dpll_ffo_param *ffo,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ enum dpll_pin_state state;
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ /*
+ * Publish FFO only for the input the DPLL is actively tracking.
+ * selected_ref alone is not enough (free-run, LOL, holdover), so use
+ * the same CONNECTED criterion as the generic selection-state logic.
+ */
+ sit9531x_dpll_selection_state_get(sitdev, sitdpll, dpin->id, &state);
+ if (state != DPLL_PIN_STATE_CONNECTED) {
+ mutex_unlock(&sitdev->multiop_lock);
+ return -ENODATA;
+ }
+
+ rc = sit9531x_pll_ffo_ppt(sitdev, sitdpll->id, &ffo->ffo);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc && rc != -ENODATA)
+ NL_SET_ERR_MSG(extack,
+ "Failed to measure the frequency offset of the selected reference");
+
+ return rc;
+}
+
/*
* sit9531x_dpll_input_pin_phase_offset_get - phase offset of a reference
*
@@ -763,6 +807,13 @@ static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
.prio_get = sit9531x_dpll_input_pin_prio_get,
.prio_set = sit9531x_dpll_input_pin_prio_set,
.phase_offset_get = sit9531x_dpll_input_pin_phase_offset_get,
+ /*
+ * The measurement compares the PLL's running feedback divider with
+ * its configured one, so it describes the device's own reference
+ * rather than a port rate.
+ */
+ .supported_ffo = BIT(DPLL_FFO_PIN_DEVICE),
+ .ffo_get = sit9531x_dpll_input_pin_ffo_get,
};
/*
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index a9731c360bc0..4a928126473d 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -291,6 +291,20 @@
#define SIT9531X_TDC_SIGN_BIT 3
#define SIT9531X_TDC_MAG_HI_MASK GENMASK(2, 0)
+/*
+ * Read codes of the running DIVN taps. Unlike the configuration
+ * registers these report what the digital loop currently commands, so
+ * they carry the correction the loop applies to track its reference.
+ * The integer part and the numerator share one tap, the denominator
+ * has its own.
+ */
+#define SIT9531X_DBG_READ_CODE_DIVN 0x57
+#define SIT9531X_DBG_READ_CODE_DIVN_DEN 0x56
+#define SIT9531X_DIVN_RT_NUM_BITS 48
+#define SIT9531X_DIVN_RT_INT_HI_BIT BIT(0)
+/* Bit 48 of the denominator tap, which lands in the same byte position */
+#define SIT9531X_DIVN_RT_DEN_HI_BIT BIT(0)
+
/*
* DIVN carried as fixed point, and the unit the DPLL ABI wants the
* fractional frequency offset in. Equal in value, distinct in meaning.
--
2.43.0
^ permalink raw reply [flat|nested] 28+ messages in thread* Re: [PATCH v10 12/14] dpll: sit9531x: add support to get fractional frequency offset
2026-09-21 20:11 ` [PATCH v10 12/14] dpll: sit9531x: add support to get fractional frequency offset Ali Rouhi
@ 2026-09-26 2:34 ` Jakub Kicinski
0 siblings, 0 replies; 28+ messages in thread
From: Jakub Kicinski @ 2026-09-26 2:34 UTC (permalink / raw)
To: arouhi
Cc: Jakub Kicinski, jiri, vadim.fedorenko, arkadiusz.kubalewski,
ivecera, robh, krzk+dt, conor+dt, cjubran, pabeni,
Oleg.Zadorozhnyi, prabhakar.mahadev-lad.rj, dev, devicetree,
netdev, linux-kernel
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
dpll: sit9531x: add support to get fractional frequency offset
This patch adds an ffo_get callback for sit9531x input pins. The offset
is computed from the ratio between the running DIVN, which is sampled
through the debug window, and the configured DIVN read from the
registers. A new helper, sit9531x_dbg_sample(), does the unlock, tap
select, triple latch and readout of the debug window.
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index 8f6f8ffc8dc13..1cb691f040194 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -1396,6 +1396,93 @@ static int sit9531x_is_xo_doubler_enabled(struct sit9531x_dev *sitdev)
[ ... ]
> + rc = sit9531x_update_pll_u8(sitdev, pll_idx,
> + SIT9531X_PLL_REG_DBG_WRITE_CODE,
> + SIT9531X_DBG_LOW_FREQ_CLK_BIT,
> + low_freq_clk ?
> + SIT9531X_DBG_LOW_FREQ_CLK_BIT : 0);
> + if (rc)
> + goto relock;
> +
> + rc = sit9531x_write_pll_u8(sitdev, pll_idx,
> + SIT9531X_PLL_REG_DBG_READ_CODE, read_code);
> + if (rc)
> + goto relock;
[Severity: Low]
Should sit9531x_dbg_sample() restore DBG_WRITE_CODE and DBG_READ_CODE
before it re-locks, the way sit9531x_phase_offset_read() does?
sit9531x_phase_offset_read() saves both registers and puts them back:
if (have_old) {
sit9531x_write_pll_u8(sitdev, pll_idx,
SIT9531X_PLL_REG_DBG_READ_CODE,
old_read_code);
Its comment says that "leaving it parked ... 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".
After each FFO read through sit9531x_divn_runtime(), DBG_READ_CODE stays
at 0x56 (SIT9531X_DBG_READ_CODE_DIVN_DEN) and
SIT9531X_DBG_LOW_FREQ_CLK_BIT stays cleared. Inside the driver this has
no effect today, because sit9531x_phase_offset_read() sets both
registers before it samples.
The comment below also says the triple latch is done "as
sit9531x_phase_offset_read() already did". However,
sit9531x_phase_offset_read() still open-codes the same
unlock/select/trigger/read/relock sequence instead of calling this
helper. That gives the driver two implementations of one debug-window
protocol, with opposite rules about leftover mux state.
Could both paths share sit9531x_dbg_sample() with one policy?
[ ... ]
> @@ -1477,6 +1564,136 @@ static int sit9531x_divn_static(struct sit9531x_dev *sitdev, u8 pll_idx,
[ ... ]
> +/**
> + * sit9531x_pll_ffo_ppt - fractional frequency offset of a PLL's reference
> + * @sitdev: device pointer
> + * @pll_idx: PLL index (0-3)
> + * @ffo: result in parts per trillion
> + *
> + * A locked PLL commands whatever DIVN keeps its VCO tracking the
> + * reference. How far that sits from the configured DIVN is how far the
> + * reference sits from the local oscillator, which is the fractional
> + * frequency offset the DPLL ABI reports for the pin feeding the device.
[Severity: Medium]
Is this the quantity the uAPI defines for DPLL_FFO_PIN_DEVICE?
Since Fvco = Fxo * DIVN, (running - configured) / configured is how far
the DPLL output sits from its nominal frequency, measured against the
local XO.
Documentation/netlink/specs/dpll.yaml describes
fractional-frequency-offset-ppt like this:
the pin-parent-device nest it represents the frequency
offset between the pin and its parent DPLL device.
For pins of type PIN_TYPE_INT_NCO this represents
the DPLL's current output frequency offset from its
nominal frequency.
zl3073x follows this split. For input pins it reads dpll_df_offset with
ZL_DPLL_DF_READ_REF_OFST set. Commit 54e65df8cf18a7 describes that
value as "the actual frequency difference between the DPLL and its
active input". The master-clock-relative value (ref_ofst=0) is used
only for the NCO pin.
The value computed here looks like the master-clock-relative one. On a
locked loop, zl3073x reports close to zero on the input pin, but
sit9531x would report the offset of the reference from the XO, which
can be many ppm.
sit9531x_dpll_input_pin_ops publishes this value as
BIT(DPLL_FFO_PIN_DEVICE). Would userspace reading the pin-parent-device
FFO then get different physical quantities from the two drivers?
The commit message makes the same claim:
the difference between the divider the loop is running and the
divider the configuration asked for is exactly the offset of the
reference.
[ ... ]
> + rc = sit9531x_divn_static(sitdev, pll_idx, &configured);
> + if (rc)
> + return rc;
> + /*
> + * Scaling by a divider far below one whole cycle overflows the
> + * 64-bit product and comes back as ~0, which would be reported
> + * as a plausible offset. A DIVN that small is not a running
> + * loop, so treat it as no measurement.
> + */
> + if (configured < SIT9531X_DIVN_SCALE)
> + return -ENODATA;
> +
> + rc = sit9531x_divn_runtime(sitdev, pll_idx, &running);
> + if (rc)
> + return rc;
> +
> + delta = running - configured;
> + magnitude = mul_u64_u64_div_u64(abs(delta), SIT9531X_PPT_PER_UNIT,
> + (u64)configured);
[Severity: Medium]
Can a negative configured DIVN get past this guard?
configured is s64 and SIT9531X_DIVN_SCALE is 1000000000000ULL, so the
comparison is done as unsigned. A negative configured then compares as
a value close to 2^64.
sit9531x_divn_static() only rejects |fracn| >= fracd. If DIVN_INT reads
0 and DIVN_NUM is negative, sit9531x_divn_fixed() returns a negative
value. For example, INT=0, NUM=-5 and a raw DEN of 10 give a configured
value of about -4.5e11.
That value passes the guard. (u64)configured is then about
2^64 - 4.5e11, so mul_u64_u64_div_u64() returns a small ppt value that
looks plausible. The kernel-doc instead promises -ENODATA for "a
configured divider below one whole unit".
This register state probably only shows up with misprogrammed registers
or bad bus reads. Was a signed comparison, e.g. against
(s64)SIT9531X_DIVN_SCALE, intended here?
[ ... ]
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index 5f7c2d01562c9..fcdd19e0bd3b0 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
> @@ -638,6 +638,50 @@ sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
[ ... ]
> + mutex_lock(&sitdev->multiop_lock);
> +
> + /*
> + * Publish FFO only for the input the DPLL is actively tracking.
> + * selected_ref alone is not enough (free-run, LOL, holdover), so use
> + * the same CONNECTED criterion as the generic selection-state logic.
> + */
[Severity: Medium]
Does the CONNECTED test actually exclude LOL and holdover?
sit9531x_dpll_selection_state_get() computes:
active_input = !chan->mode && chan->selected_ref == pin_id;
Apart from that it only checks prio_mask. It never reads chan->locked,
chan->inner_lol or chan->ho_freeze, so free-run is the only case it
filters out.
The pin-state contract comment in dpll.c defines CONNECTED as
A && L && S && M, with L = chan->locked && !chan->inner_lol. The code
does not implement L.
The running DIVN is not settled in these cases:
- the loop is still acquiring lock (chan->mode == 0, !chan->locked)
- the loop has lost lock
- the PLL is in holdover, if ACTIVESEL still reports the last
reference
The kernel-doc of sit9531x_pll_ffo_ppt() assumes "A locked PLL".
Could this report an FFO for a reference the loop is not tracking, with
nothing to show that the value is invalid?
> + sit9531x_dpll_selection_state_get(sitdev, sitdpll, dpin->id, &state);
> + if (state != DPLL_PIN_STATE_CONNECTED) {
> + mutex_unlock(&sitdev->multiop_lock);
> + return -ENODATA;
> + }
[Severity: Medium]
Can this report the measurement on the wrong pin after the device
switches reference by itself?
The CONNECTED state comes from the cached chan->selected_ref. The
monitor refreshes that only every SIT9531X_STATUS_POLL_MS (500 ms).
The sibling sit9531x_dpll_input_pin_phase_offset_get() reads the live
selection first for this reason:
rc = sit9531x_chan_selected_ref_read(sitdev, sitdpll->id,
&selected);
...
if (state != DPLL_PIN_STATE_CONNECTED || selected != dpin->id) {
The kernel-doc of sit9531x_chan_selected_ref_read() says the cache is
"close enough for reporting pin state but not for attributing a
measurement".
Take a failover where the higher-priority input hits LOS. Until the next
poll, a pin-get on the old pin still sees CONNECTED. At that point
sit9531x_pll_ffo_ppt() samples a running DIVN that already tracks the
new reference.
The function's own comment says it avoids "reporting the active
reference's figure against the wrong pin". Should ffo_get also call
sit9531x_chan_selected_ref_read() and require selected == dpin->id?
[ ... ]
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH v10 13/14] dpll: sit9531x: model the inter-PLL sync net as a pair of pins
2026-09-21 20:11 [PATCH v10 00/14] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (11 preceding siblings ...)
2026-09-21 20:11 ` [PATCH v10 12/14] dpll: sit9531x: add support to get fractional frequency offset Ali Rouhi
@ 2026-09-21 20:11 ` Ali Rouhi
2026-09-26 2:34 ` Jakub Kicinski
2026-09-21 20:11 ` [PATCH v10 14/14] dpll: sit9531x: allow the device tree to override two board facts Ali Rouhi
13 siblings, 1 reply; 28+ messages in thread
From: Ali Rouhi @ 2026-09-21 20:11 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi,
prabhakar.mahadev-lad.rj, dev, devicetree, netdev, linux-kernel,
Ali Rouhi
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
The device has an internal net by which one PLL can drive the others: the
source PLL puts its output on it, and any other PLL can select it as a
reference instead of an external input. The two ends are nothing alike --
one is driven, the other is selected -- so they are two pins rather than
one: an output pin on the source and an input pin on each destination.
That keeps each pin honest about what its state means. The source pin
reports whether this PLL is the one driving the net, and setting it takes
the net over or gives it up; a destination pin reports whether its PLL has
selected the net, and behaves like any other selectable input. A single
pin would have had to answer both questions at once and could only have
been right about one of them.
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
Notes:
Changes in v10:
INTSYNC ownership: recorded before the read-back that can fail, and a
disable that fails part way leaves the state describable so the request
can be repeated.
Error reporting: a full priority table says so instead of reading like a
bus failure.
drivers/dpll/sit9531x/core.c | 291 ++++++++++++++++++++++++++++++++++-
drivers/dpll/sit9531x/core.h | 3 +
drivers/dpll/sit9531x/dpll.c | 247 ++++++++++++++++++++++++++++-
drivers/dpll/sit9531x/regs.h | 3 +
4 files changed, 538 insertions(+), 6 deletions(-)
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 1cb691f04019..e8d47999f1a9 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -2264,9 +2264,9 @@ int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
* 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,
@@ -2531,6 +2531,278 @@ int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
return 0;
}
+/*
+ * INTSYNC configuration register values.
+ * These are written to the source PLL's EXT page to enable/disable
+ * inter-PLL synchronization (lock frequency PLL to phase PLL).
+ */
+struct sit9531x_intsync_reg {
+ u8 offset;
+ u8 en_val;
+ u8 dis_val;
+};
+
+static const struct sit9531x_intsync_reg intsync_config[] = {
+ { 0x2D, 0x02, 0x00 },
+ { 0x50, 0x08, 0x00 },
+ { 0x51, 0x04, 0x00 },
+ { 0x54, 0x02, 0x00 },
+ { 0x55, 0x28, 0x20 },
+ { 0x5C, 0x0F, 0x00 },
+ { 0x5D, 0xFF, 0x00 },
+ { 0x6C, 0xDD, 0x00 },
+};
+
+int sit9531x_intsync_src_detect(struct sit9531x_dev *sitdev)
+{
+ s8 src = -1;
+ u8 global;
+ u8 pll, ext_page;
+ int rc, i;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &global);
+ if (rc)
+ return rc;
+
+ if (!(global & BIT(SIT9531X_INTSYNC_EN_BIT))) {
+ sitdev->intsync_src = -1;
+ return 0;
+ }
+
+ for (pll = 0; pll < SIT9531X_NUM_PLLS; pll++) {
+ ext_page = SIT9531X_PLL_EXT_PAGE(pll);
+
+ for (i = 0; i < ARRAY_SIZE(intsync_config); i++) {
+ u16 reg;
+ u8 val;
+
+ reg = SIT9531X_REG(ext_page, intsync_config[i].offset);
+
+ rc = sit9531x_read_u8(sitdev, reg, &val);
+ if (rc)
+ return rc;
+ if (val != intsync_config[i].en_val)
+ break;
+ }
+
+ if (i == ARRAY_SIZE(intsync_config)) {
+ /*
+ * Only one PLL can drive the net. If a second
+ * one matches, the registers are not describing
+ * a state this driver put the device in, so say
+ * so rather than pick silently.
+ */
+ if (src < 0)
+ src = pll;
+ else
+ dev_warn(sitdev->dev,
+ "PLL%c also matches the INTSYNC source pattern; keeping PLL%c\n",
+ 'A' + pll, 'A' + src);
+ }
+ }
+
+ sitdev->intsync_src = src;
+
+ return 0;
+}
+
+/*
+ * 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);
+}
+
+/*
+ * 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, 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_write_pll_u8(sitdev, src_pll_idx,
+ SIT9531X_PLL_REG_SMALL_UPDATE,
+ SIT9531X_SMALL_UPDATE_CMD);
+ 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.
+ */
+ lock_rc = sit9531x_intsync_debug_lock(sitdev, ext_page);
+ if (lock_rc && !rc)
+ rc = 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, i;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (src_pll_idx >= SIT9531X_NUM_PLLS)
+ return -EINVAL;
+
+ ext_page = SIT9531X_PLL_EXT_PAGE(src_pll_idx);
+
+ rc = sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &val);
+ if (rc)
+ return rc;
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL,
+ val & ~BIT(SIT9531X_INTSYNC_EN_BIT));
+ if (rc)
+ return rc;
+
+ /* Small update on Page 0 */
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+ SIT9531X_SMALL_UPDATE_CMD);
+ usleep_range(1000, 2000);
+ if (rc)
+ return rc;
+
+ /* Unlock debug on EXT page */
+ rc = sit9531x_write_u8(sitdev,
+ SIT9531X_REG(ext_page,
+ SIT9531X_PLL_REG_DEBUG),
+ SIT9531X_PLL_DEBUG_UNLOCK);
+ if (rc)
+ goto relock;
+
+ 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_write_pll_u8(sitdev, src_pll_idx,
+ SIT9531X_PLL_REG_SMALL_UPDATE,
+ SIT9531X_SMALL_UPDATE_CMD);
+ if (rc)
+ goto relock;
+
+ rc = 0;
+
+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. */
+ lock_rc = sit9531x_intsync_debug_lock(sitdev, ext_page);
+ if (lock_rc && !rc)
+ rc = lock_rc;
+
+ return rc;
+}
+
/**
* sit9531x_chan_selected_ref_read - read a PLL's active reference now
* @sitdev: device pointer
@@ -3109,6 +3381,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;
@@ -3664,13 +3945,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 1d77b89e83ec..1fc14eabb621 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -287,6 +287,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_pll_ffo_ppt(struct sit9531x_dev *sitdev, u8 pll_idx, s64 *ffo);
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index fcdd19e0bd3b..856c9bdf33d4 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)
{
@@ -843,8 +857,234 @@ 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;
+ 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);
+ break;
+ case DPLL_PIN_STATE_DISCONNECTED:
+ if (sitdev->intsync_src != sitdpll->id)
+ break;
+ rc = sit9531x_intsync_disable(sitdev, sitdpll->id);
+ break;
+ default:
+ rc = -EINVAL;
+ break;
+ }
+
+ /*
+ * Re-scan hardware after source state transitions so cache follows
+ * partially failed enable/disable paths as closely as possible.
+ */
+ /*
+ * Record what was asked for before confirming it. The refresh below
+ * leaves the cache untouched when a read fails, and a cache that
+ * still says nobody drives the net would let a second PLL be
+ * configured to drive it as well.
+ */
+ if (!rc && state == DPLL_PIN_STATE_CONNECTED)
+ sitdev->intsync_src = sitdpll->id;
+ else if (!rc && state == DPLL_PIN_STATE_DISCONNECTED)
+ sitdev->intsync_src = -1;
+
+ if (state == DPLL_PIN_STATE_CONNECTED ||
+ state == DPLL_PIN_STATE_DISCONNECTED)
+ detect_rc = sit9531x_intsync_src_detect(sitdev);
+ /*
+ * The refresh only re-reads what the device now shows. Failing
+ * the request because that read hit a bus error would tell
+ * userspace the enable did not happen when it did.
+ */
+ if (detect_rc)
+ dev_warn(sitdev->dev,
+ "INTSYNC source cache not refreshed: %d\n",
+ detect_rc);
+
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc && rc != -EBUSY && rc != -EINVAL && rc != -EOPNOTSUPP)
+ NL_SET_ERR_MSG(extack, "Failed to set INTSYNC source state");
+
+ return rc;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_intsync_src_pin_ops = {
+ .direction_get = sit9531x_dpll_output_pin_direction_get,
+ .state_on_dpll_get = sit9531x_dpll_intsync_src_state_on_dpll_get,
+ .state_on_dpll_set = sit9531x_dpll_intsync_src_state_on_dpll_set,
+};
+
/* ---- INTSYNC destination (input) pin ---- */
+/*
+ * sit9531x_dpll_intsync_dst_state_on_dpll_get - INTSYNC reference state
+ *
+ * Selection role, so the contract above decides this exactly as it does
+ * for a physical input: the priority table is the eligibility record, and
+ * whether a source PLL happens to be driving the net right now is no more
+ * a state than a momentary LOS is on an external reference. The one
+ * addition is that the PLL driving INTSYNC is never its own destination.
+ */
+static int
+sit9531x_dpll_intsync_dst_state_on_dpll_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state *state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+
+ mutex_lock(&sitdev->multiop_lock);
+ if (sitdev->intsync_src == sitdpll->id)
+ *state = DPLL_PIN_STATE_DISCONNECTED;
+ else
+ sit9531x_dpll_selection_state_get(sitdev, sitdpll,
+ SIT9531X_INTSYNC_PIN_ID,
+ state);
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return 0;
+}
+
+/*
+ * sit9531x_dpll_intsync_dst_state_on_dpll_set - lock a PLL to INTSYNC
+ *
+ * Selection role, so this accepts and refuses what a physical input does,
+ * CONNECTED included: the device pins no reference on request whichever
+ * source is asked for. INTSYNC is an internal net with no physical
+ * receiver, so only the per-PLL priority table is touched; the source pin
+ * controls generation.
+ */
+static int
+sit9531x_dpll_intsync_dst_state_on_dpll_set(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state state,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ u8 hw_src = sit9531x_input_hw_src(SIT9531X_INTSYNC_PIN_ID);
+ int rc;
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ switch (state) {
+ case DPLL_PIN_STATE_DISCONNECTED:
+ rc = sit9531x_input_prio_remove(sitdev, sitdpll->id, hw_src);
+ break;
+ case DPLL_PIN_STATE_CONNECTED:
+ NL_SET_ERR_MSG(extack,
+ "Device selects its reference by priority; use selectable");
+ rc = -EOPNOTSUPP;
+ break;
+ case DPLL_PIN_STATE_SELECTABLE:
+ if (sitdev->intsync_src == sitdpll->id) {
+ NL_SET_ERR_MSG(extack,
+ "PLL cannot lock to the INTSYNC it drives");
+ rc = -EINVAL;
+ break;
+ }
+ rc = sit9531x_input_prio_add(sitdev, sitdpll->id, hw_src);
+ break;
+ default:
+ rc = -EINVAL;
+ break;
+ }
+
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc == -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;
+}
+
+/*
+ * Do not add .frequency_get / the generic input state getter here: the
+ * destination pin id is SIT9531X_INTSYNC_PIN_ID, one past the end of the
+ * ref[] array (INTSYNC is an internal net with no ref[] entry). The ops
+ * below only ever key on chan[] and the priority table, never ref[id].
+ */
+static const struct dpll_pin_ops sit9531x_dpll_intsync_dst_pin_ops = {
+ .direction_get = sit9531x_dpll_input_pin_direction_get,
+ .state_on_dpll_get = sit9531x_dpll_intsync_dst_state_on_dpll_get,
+ .state_on_dpll_set = sit9531x_dpll_intsync_dst_state_on_dpll_set,
+ .prio_get = sit9531x_dpll_input_pin_prio_get,
+ .prio_set = sit9531x_dpll_input_pin_prio_set,
+};
+
/*
* XO (crystal oscillator) pin ops
*
@@ -1126,8 +1366,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 4a928126473d..09a2032f7bf0 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -315,6 +315,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] 28+ messages in thread* Re: [PATCH v10 13/14] dpll: sit9531x: model the inter-PLL sync net as a pair of pins
2026-09-21 20:11 ` [PATCH v10 13/14] dpll: sit9531x: model the inter-PLL sync net as a pair of pins Ali Rouhi
@ 2026-09-26 2:34 ` Jakub Kicinski
0 siblings, 0 replies; 28+ messages in thread
From: Jakub Kicinski @ 2026-09-26 2:34 UTC (permalink / raw)
To: arouhi
Cc: Jakub Kicinski, jiri, vadim.fedorenko, arkadiusz.kubalewski,
ivecera, robh, krzk+dt, conor+dt, cjubran, pabeni,
Oleg.Zadorozhnyi, prabhakar.mahadev-lad.rj, dev, devicetree,
netdev, linux-kernel
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
dpll: sit9531x: model the inter-PLL sync net as a pair of pins
This patch models the device's internal inter-PLL sync (INTSYNC) net as
two DPLL pins: an output pin on the source PLL and an input pin on each
destination PLL. It adds sit9531x_intsync_enable(),
sit9531x_intsync_disable() and sit9531x_intsync_src_detect() in core.c,
and dpll.c gets the pin ops that use them.
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index 1cb691f040194..e8d47999f1a97 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
> @@ -2264,9 +2264,9 @@ int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
> * block at base = 0x15 + 16 * (slot % 6); the slot is the physical
> * output position from clkout_map[], not the logical output index.
> *
> - * The chip only supports unsigned positive delay. A negative phase
> - * adjustment (advance) is wrapped to (T_out - |phase|) modulo one
> - * output period, which is identical for a periodic signal.
> + * The chip only supports unsigned positive delay. Requests are folded
> + * modulo one output period: positive delays wrap naturally and a negative
> + * phase adjustment (advance) is rendered as (T_out - |phase|).
> */
[Severity: Low]
This isn't a bug, but this comment rewrite for
sit9531x_output_phase_adjust_set() has nothing to do with INTSYNC, and
neither the commit message nor the v10 notes mention it. Would it fit
better in "dpll: sit9531x: add support to adjust output phase", or in its
own patch?
[ ... ]
> @@ -2531,6 +2531,278 @@ int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
[ ... ]
> +int sit9531x_intsync_src_detect(struct sit9531x_dev *sitdev)
> +{
[ ... ]
> + for (pll = 0; pll < SIT9531X_NUM_PLLS; pll++) {
> + ext_page = SIT9531X_PLL_EXT_PAGE(pll);
> +
> + for (i = 0; i < ARRAY_SIZE(intsync_config); i++) {
> + u16 reg;
> + u8 val;
> +
> + reg = SIT9531X_REG(ext_page, intsync_config[i].offset);
> +
> + rc = sit9531x_read_u8(sitdev, reg, &val);
> + if (rc)
> + return rc;
> + if (val != intsync_config[i].en_val)
> + break;
> + }
[Severity: Medium]
Do these EXT page reads return the real register contents while the debug
window is locked?
sit9531x_intsync_enable() and sit9531x_intsync_disable() both write
SIT9531X_PLL_DEBUG_UNLOCK before they touch these offsets.
sit9531x_phase_offset_read() also unlocks before it reads:
/* Unlock the debug page so the TDC registers are accessible. */
A locked read might return 0 or the lock value, or one of these bytes
might carry status bits. In any of those cases the exact en_val match
here never succeeds.
In sit9531x_dpll_intsync_src_state_on_dpll_set(), a successful enable
first records sitdev->intsync_src = sitdpll->id. The detect call that
follows would then overwrite it with -1.
The SYNCOUT pin would then read DISCONNECTED while the net is driven. The
"INTSYNC is already sourced by another PLL" check would also stop
blocking a second PLL.
The same applies at probe in sit9531x_dev_state_fetch(). A source that
the loaded configuration already set up would not be recognised.
[ ... ]
> +int sit9531x_intsync_enable(struct sit9531x_dev *sitdev, u8 src_pll_idx)
> +{
[ ... ]
> + /* Small update on source PLL */
> + rc = sit9531x_write_pll_u8(sitdev, src_pll_idx,
> + SIT9531X_PLL_REG_SMALL_UPDATE,
> + SIT9531X_SMALL_UPDATE_CMD);
> + if (rc)
> + goto 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.
> + */
> + lock_rc = sit9531x_intsync_debug_lock(sitdev, ext_page);
> + if (lock_rc && !rc)
> + rc = lock_rc;
> +
> + return rc;
[Severity: Medium]
What happens if only this last sit9531x_intsync_debug_lock() fails?
By this point the global bit is set, the EXT page holds en_val and the
source PLL small update has been applied, so the net is being driven.
The function still returns lock_rc. Unlike the relock_err paths, it does
no rollback.
The caller then skips recording the owner:
if (!rc && state == DPLL_PIN_STATE_CONNECTED)
sitdev->intsync_src = sitdpll->id;
dpll_pin_state_set() then returns the error to userspace and sends no
change notification, even though the enable took effect.
Only the following sit9531x_intsync_src_detect() can record the owner.
If that read also fails, intsync_src stays -1 while the net is driven,
and another PLL can get past the -EBUSY check.
[ ... ]
> +int sit9531x_intsync_disable(struct sit9531x_dev *sitdev, u8 src_pll_idx)
> +{
[ ... ]
> + rc = sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &val);
> + if (rc)
> + return rc;
> + rc = sit9531x_write_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL,
> + val & ~BIT(SIT9531X_INTSYNC_EN_BIT));
> + if (rc)
> + return rc;
> +
> + /* Small update on Page 0 */
> + rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
> + SIT9531X_SMALL_UPDATE_CMD);
> + usleep_range(1000, 2000);
> + if (rc)
> + return rc;
> +
> + /* Unlock debug on EXT page */
> + rc = sit9531x_write_u8(sitdev,
> + SIT9531X_REG(ext_page,
> + SIT9531X_PLL_REG_DEBUG),
> + SIT9531X_PLL_DEBUG_UNLOCK);
> + if (rc)
> + goto relock;
> +
> + 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_write_pll_u8(sitdev, src_pll_idx,
> + SIT9531X_PLL_REG_SMALL_UPDATE,
> + SIT9531X_SMALL_UPDATE_CMD);
> + if (rc)
> + goto relock;
> +
> + rc = 0;
> +
> +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: High]
Is a goto relock missing after rc = 0 here?
sit9531x_intsync_enable() has:
rc = 0;
goto relock;
In sit9531x_intsync_disable(), though, the success path falls straight
into restore_global. That code reads SIT9531X_REG_INTSYNC_GLOBAL and
writes SIT9531X_INTSYNC_EN_BIT back. Every successful disable therefore
ends with the global enable set again while the EXT page holds the
disable pattern.
No page-0 small update follows this write, so the next unrelated one
latches it. One example is the SIT9531X_REG_GLOBAL_UPDATE write that
sit9531x_prio_prg_commit() does on every priority table change.
The rollback in sit9531x_intsync_enable() goes through the same path:
rollback_rc = sit9531x_intsync_disable(sitdev, src_pll_idx);
A failed enable therefore also leaves the global bit asserted. The
err_disable comment says the rollback is there to prevent exactly that.
This also doesn't match the kernel-doc ("Clears INTSYNC global bit") or
the commit message, which says setting the source pin "gives it up".
After the disable, sit9531x_intsync_src_detect() finds the enable bit set
but no matching PLL, and reports -1. Userspace sees DISCONNECTED while
the hardware global enable stays on.
> +
> +relock:
> + /* Close the EXT page debug window the sequence opened. */
> + lock_rc = sit9531x_intsync_debug_lock(sitdev, ext_page);
> + if (lock_rc && !rc)
> + rc = lock_rc;
> +
> + return rc;
> +}
[Severity: Medium]
Does this error handling cover every partial failure that the v10 notes
describe ("a disable that fails part way leaves the state describable so
the request can be repeated")?
If the page-0 small update fails, the function returns directly. If the
EXT debug unlock fails, it jumps to relock. Both paths skip
restore_global, which leaves the global bit clear while the EXT page
still holds the full en_val pattern.
If a dis_val write fails part way through intsync_config[], the earlier
entries already hold dis_val. restore_global puts the global bit back,
but no PLL fully matches in sit9531x_intsync_src_detect() any more, and
no small update is issued.
If the source PLL small update fails, the function jumps to relock. The
global bit is clear and the disable pattern is staged but not applied.
In each case sit9531x_dpll_intsync_src_state_on_dpll_set() skips the
ownership update. It still calls sit9531x_intsync_src_detect(), though,
and that sets intsync_src to -1, either here:
if (!(global & BIT(SIT9531X_INTSYNC_EN_BIT))) {
sitdev->intsync_src = -1;
return 0;
}
or because no PLL fully matches.
A retried DISCONNECTED then hits:
if (sitdev->intsync_src != sitdpll->id)
break;
It returns 0 without touching the hardware, so the leftover EXT page
configuration is never cleaned up.
With the cache at -1, a CONNECTED on another PLL gets past the -EBUSY
check. sit9531x_intsync_enable() on that PLL sets and applies the global
bit, which also reactivates the first PLL's leftover configuration.
Could that leave two PLLs configured to drive the net?
[ ... ]
> diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
> index fcdd19e0bd3b0..856c9bdf33d45 100644
> --- a/drivers/dpll/sit9531x/dpll.c
> +++ b/drivers/dpll/sit9531x/dpll.c
[ ... ]
> @@ -843,8 +857,234 @@ 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,
[ ... ]
> + case DPLL_PIN_STATE_DISCONNECTED:
> + if (sitdev->intsync_src != sitdpll->id)
> + break;
> + rc = sit9531x_intsync_disable(sitdev, sitdpll->id);
> + break;
[ ... ]
> + if (!rc && state == DPLL_PIN_STATE_CONNECTED)
> + sitdev->intsync_src = sitdpll->id;
> + else if (!rc && state == DPLL_PIN_STATE_DISCONNECTED)
> + sitdev->intsync_src = -1;
> +
> + if (state == DPLL_PIN_STATE_CONNECTED ||
> + state == DPLL_PIN_STATE_DISCONNECTED)
> + detect_rc = sit9531x_intsync_src_detect(sitdev);
[Severity: Medium]
Can a DISCONNECTED request on a PLL that doesn't own the net wipe the
real owner from the cache?
dpll_pin_state_set() passes the request straight to the driver and does
not filter out requests for the state the pin is already in. Say
intsync_src is PLL B and the request is for PLL A. The DISCONNECTED case
breaks out with rc == 0, and then this runs:
else if (!rc && state == DPLL_PIN_STATE_DISCONNECTED)
sitdev->intsync_src = -1;
That happens even though A never owned the net.
Only the following sit9531x_intsync_src_detect() can put B back. If one
of its reads fails, it returns before writing intsync_src, and the caller
only warns. The cache then stays -1 while B drives the net.
A later CONNECTED on PLL C would then get past the -EBUSY check and
enable C as a second source.
Should the cache only change when this PLL actually changed state?
[ ... ]
> +static int
> +sit9531x_dpll_intsync_dst_state_on_dpll_set(const struct dpll_pin *pin,
[ ... ]
> + switch (state) {
> + case DPLL_PIN_STATE_DISCONNECTED:
> + rc = sit9531x_input_prio_remove(sitdev, sitdpll->id, hw_src);
> + break;
[Severity: Low]
Should this path notify the sibling input pins whose priorities change?
sit9531x_input_prio_remove() compacts the remaining entries toward the
head of the table (kept[count++]), so every reference listed after
INTSYNC moves up one slot. dpll_pin_state_set() only notifies the
INTSYNC pin itself.
sit9531x_dpll_input_pin_prio_set() walks sitdpll->pins and calls
__dpll_pin_change_ntf() for each sibling whose slot moved. Nothing here
does that, so subscribers keep stale priorities for the siblings.
The existing DISCONNECTED path for physical inputs seems to have the
same gap, and this new setter copies it.
[ ... ]
> +static const struct dpll_pin_ops sit9531x_dpll_intsync_dst_pin_ops = {
> + .direction_get = sit9531x_dpll_input_pin_direction_get,
> + .state_on_dpll_get = sit9531x_dpll_intsync_dst_state_on_dpll_get,
> + .state_on_dpll_set = sit9531x_dpll_intsync_dst_state_on_dpll_set,
> + .prio_get = sit9531x_dpll_input_pin_prio_get,
> + .prio_set = sit9531x_dpll_input_pin_prio_set,
> +};
[Severity: High]
This isn't a bug introduced by this patch, but reusing
sit9531x_dpll_input_pin_prio_set() here extends a pre-existing race with
teardown to the new pin.
sit9531x_dpll_pins_unregister() does this without holding dpll_lock or
multiop_lock around the list_del() and kfree():
list_for_each_entry_safe(pin, next, &sitdpll->pins, list) {
sit9531x_dpll_pin_unregister(pin);
list_del(&pin->list);
kfree(pin);
}
Once dpll_pin_unregister() has returned for one pin, a netlink prio_set
on a pin that is still registered can run
sit9531x_dpll_input_pin_prio_set(). That function walks the same list:
list_for_each_entry(sibling, &sitdpll->pins, list) {
It reads and writes sibling->dir, id and prio as it goes.
The check below only avoids notifying through a NULL dpll_pin:
if (sibling == dpin || !sibling->dpll_pin)
continue;
Could the walk reach a pin that the teardown thread has just freed, or
follow LIST_POISON links?
This patch adds both INTSYNC pins to each sitdpll->pins list and gives
the destination pin this prio_set callback.
[ ... ]
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH v10 14/14] dpll: sit9531x: allow the device tree to override two board facts
2026-09-21 20:11 [PATCH v10 00/14] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
` (12 preceding siblings ...)
2026-09-21 20:11 ` [PATCH v10 13/14] dpll: sit9531x: model the inter-PLL sync net as a pair of pins Ali Rouhi
@ 2026-09-21 20:11 ` Ali Rouhi
2026-09-26 2:34 ` Jakub Kicinski
13 siblings, 1 reply; 28+ messages in thread
From: Ali Rouhi @ 2026-09-21 20:11 UTC (permalink / raw)
To: jiri
Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, robh, krzk+dt,
conor+dt, cjubran, pabeni, Oleg.Zadorozhnyi,
prabhakar.mahadev-lad.rj, dev, devicetree, netdev, linux-kernel,
Ali Rouhi
From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Two things the driver reads from the chip can be wrong on a board, and
neither has anywhere else to come from.
The VCO frequency is derived from the feedback divider, which is exact
while the loop runs but not while a PLL sits in free-run with a divider
the configuration never programmed; a board that knows its own VCO can
state it. The output-to-PLL routing is read from the output map
registers, which describe what the loaded configuration did -- and a board
whose outputs are fanned out differently from what those registers imply
can state that too.
Both are optional. Absent the properties the driver behaves exactly as
before, deriving one and reading the other.
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
Notes:
Changes in v10:
Checked the device-tree array lengths against the part: the VCO
override needs an entry per PLL, and an output map shorter than the
variant's output count is ignored rather than read as unmapped
outputs.
Updated the accessor's kernel-doc, which no longer described the
override path.
drivers/dpll/sit9531x/core.c | 171 ++++++++++++++++++++++++++++++++++-
drivers/dpll/sit9531x/core.h | 1 +
2 files changed, 167 insertions(+), 5 deletions(-)
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index e8d47999f1a9..5316e6bcb338 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -1695,12 +1695,15 @@ int sit9531x_pll_ffo_ppt(struct sit9531x_dev *sitdev, u8 pll_idx, s64 *ffo)
}
/*
- * sit9531x_get_fvco - read VCO frequency from chip's DIVN registers
+ * sit9531x_get_fvco - the VCO frequency the driver works from
*
- * 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.
+ * A board that describes the VCO through "sitime,pll-fvco" is taken at
+ * its word and nothing is read. Otherwise Fvco = Fref * DIVN, where
+ * DIVN comes from sit9531x_divn_static() and Fref = xtal_freq <<
+ * doubler. DIVN is the steady-state Fvco/Fref target programmed by the
+ * NVM blob and is authoritative in both free-run and sync modes; the
+ * result is clamped to the band the PLL runs in, so every caller divides
+ * the same number.
*
* Return: 0 with *fvco set on success, -ENODATA when DIVN is not
* programmed (dormant PLL), or the register access error. A bus
@@ -3238,6 +3241,27 @@ static int sit9531x_out_state_fetch(struct sit9531x_dev *sitdev, u8 index)
sitdev->out[index].state_stale = false;
+ /*
+ * DT board-config override: the per-PLL OUTPUT_ENABLE bitmaps
+ * (0x27/0x28) do not unambiguously express output->PLL routing on
+ * every config (overlaps, and some outputs routed outside that
+ * path). When the board supplies an explicit map, trust it.
+ */
+ if (sitdev->out_pll_map_valid) {
+ u8 m = sitdev->out_pll_map[index];
+
+ if (m < SIT9531X_NUM_PLLS) {
+ out->pll_idx = m;
+ out->routed = true;
+ out->enabled = !muted;
+ } else {
+ out->pll_idx = 0;
+ out->routed = false;
+ out->enabled = false;
+ }
+ return 0;
+ }
+
/*
* The OUT_MAP_LO/HI bitmaps are indexed by the physical slot the
* output occupies on the chip, not by the driver's logical output
@@ -4223,6 +4247,126 @@ static u64 sit9531x_derive_clock_id(struct sit9531x_dev *sitdev)
return clkid;
}
+/*
+ * Does an Fvco fall in the band the given PLL runs in?
+ *
+ * The two bands are disjoint, and which one applies is fixed per PLL, so a
+ * single envelope from the bottom of the low band to the top of the high
+ * one would accept both the ~1 GHz gap between them and a rate belonging to
+ * the other PLL's band.
+ */
+static bool sit9531x_fvco_in_band(u8 pll_idx, u64 fvco)
+{
+ if (pll_idx == 1 || pll_idx == 3)
+ return fvco >= SIT9531X_FVCO_HIGHBAND_MIN &&
+ fvco <= SIT9531X_FVCO_HIGHBAND_MAX;
+
+ return fvco >= SIT9531X_FVCO_LOWBAND_MIN &&
+ fvco <= SIT9531X_FVCO_LOWBAND_MAX;
+}
+
+/*
+ * Board-config overrides for fixed efuse/blob routing the chip registers do
+ * not describe unambiguously. Absent properties leave pll_fvco[] zeroed
+ * (derive from DIVN) and out_pll_map_valid false (use the OUT_MAP registers).
+ */
+static void sit9531x_parse_board_config(struct sit9531x_dev *sitdev)
+{
+ u32 map[SIT9531X_MAX_OUTPUTS];
+ int n, i, rc;
+
+ if (device_property_present(sitdev->dev, "sitime,pll-fvco")) {
+ /*
+ * A fixed-count read rejects a short array but accepts a
+ * long one and drops the surplus, so the count is checked
+ * here: the property describes four PLLs and an array of
+ * any other length describes something else.
+ */
+ rc = device_property_count_u64(sitdev->dev,
+ "sitime,pll-fvco");
+ if (rc != SIT9531X_NUM_PLLS) {
+ dev_warn(sitdev->dev,
+ "sitime,pll-fvco needs %d entries, ignoring\n",
+ SIT9531X_NUM_PLLS);
+ memset(sitdev->pll_fvco, 0, sizeof(sitdev->pll_fvco));
+ goto out_map;
+ }
+
+ rc = device_property_read_u64_array(sitdev->dev,
+ "sitime,pll-fvco",
+ sitdev->pll_fvco,
+ SIT9531X_NUM_PLLS);
+ if (rc) {
+ dev_warn(sitdev->dev,
+ "invalid sitime,pll-fvco (%d), ignoring\n",
+ rc);
+ memset(sitdev->pll_fvco, 0, sizeof(sitdev->pll_fvco));
+ }
+
+ /*
+ * The override is used verbatim by the divider math, so an
+ * implausible value (units typo, wrong cell count worked
+ * around with zeros) must not silently misprogram DIVO.
+ * Anything outside both VCO bands is dropped with a warning
+ * rather than trusted.
+ */
+ for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+ u64 f = sitdev->pll_fvco[i];
+
+ if (f && !sit9531x_fvco_in_band(i, f)) {
+ dev_warn(sitdev->dev,
+ "PLL%c Fvco override %llu Hz is outside the band that PLL runs in, ignoring\n",
+ 'A' + i, f);
+ sitdev->pll_fvco[i] = 0;
+ }
+ }
+ }
+
+out_map:
+ if (!device_property_present(sitdev->dev, "sitime,output-pll-map"))
+ return;
+
+ /*
+ * Any 1..MAX_OUTPUTS length is accepted so the 8-output SiT95317 need
+ * not pad to 12; variant detection has not run yet and entries past
+ * the detected num_outputs are never indexed. Trailing entries of a
+ * short map must read as unmapped rather than 0 (== PLLA), which
+ * would mark unrouted outputs active in sit9531x_out_state_fetch().
+ */
+ memset(sitdev->out_pll_map, SIT9531X_OUT_PLL_UNMAPPED,
+ sizeof(sitdev->out_pll_map));
+
+ n = device_property_count_u32(sitdev->dev, "sitime,output-pll-map");
+ sitdev->out_pll_map_count = (n > 0) ? n : 0;
+ if (n <= 0 || n > SIT9531X_MAX_OUTPUTS ||
+ device_property_read_u32_array(sitdev->dev, "sitime,output-pll-map",
+ map, n)) {
+ dev_warn(sitdev->dev,
+ "invalid sitime,output-pll-map, ignoring\n");
+ return;
+ }
+
+ /*
+ * The binding allows only 0-3 and 255 per entry. A stray value
+ * would silently unroute an output (m >= SIT9531X_NUM_PLLS reads
+ * as unmapped in sit9531x_out_state_fetch()), so reject the whole
+ * property loudly instead.
+ */
+ for (i = 0; i < n; i++) {
+ if (map[i] >= SIT9531X_NUM_PLLS &&
+ map[i] != SIT9531X_OUT_PLL_UNMAPPED) {
+ dev_warn(sitdev->dev,
+ "sitime,output-pll-map entry %d is %u (must be 0-3 or 255), ignoring map\n",
+ i, map[i]);
+ return;
+ }
+ }
+
+ for (i = 0; i < n; i++)
+ sitdev->out_pll_map[i] = map[i];
+ sitdev->out_pll_map_valid = true;
+}
+
int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
{
struct clk *xtal_clk;
@@ -4267,6 +4411,8 @@ int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
if (sitdev->reset_gpio)
fsleep(10000); /* internal boot after release */
+ sit9531x_parse_board_config(sitdev);
+
rc = sit9531x_read_variant_id(sitdev, &variant_id);
if (rc)
return rc;
@@ -4277,6 +4423,21 @@ int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
"Unknown variant ID: 0x%02x\n",
variant_id);
+ /*
+ * The map is parsed before the variant is known, so its length can
+ * only be checked against the part here. A map that stops short of
+ * the outputs this variant has leaves the rest reading as unmapped,
+ * which would drop real output pins; fall back to the routing the
+ * registers describe instead.
+ */
+ if (sitdev->out_pll_map_valid &&
+ sitdev->out_pll_map_count < sitdev->info->num_outputs) {
+ dev_warn(sitdev->dev,
+ "sitime,output-pll-map has %u of %u outputs, ignoring map\n",
+ sitdev->out_pll_map_count, sitdev->info->num_outputs);
+ sitdev->out_pll_map_valid = false;
+ }
+
sitdev->clock_id = sit9531x_derive_clock_id(sitdev);
sitdev->intsync_src = -1;
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 1fc14eabb621..31a5f49c0253 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -224,6 +224,7 @@ struct sit9531x_dev {
u64 pll_fvco[SIT9531X_NUM_PLLS];
u8 out_pll_map[SIT9531X_MAX_OUTPUTS];
bool out_pll_map_valid;
+ u8 out_pll_map_count;
/* Inter-PLL synchronization state */
s8 intsync_src;
--
2.43.0
^ permalink raw reply [flat|nested] 28+ messages in thread* Re: [PATCH v10 14/14] dpll: sit9531x: allow the device tree to override two board facts
2026-09-21 20:11 ` [PATCH v10 14/14] dpll: sit9531x: allow the device tree to override two board facts Ali Rouhi
@ 2026-09-26 2:34 ` Jakub Kicinski
0 siblings, 0 replies; 28+ messages in thread
From: Jakub Kicinski @ 2026-09-26 2:34 UTC (permalink / raw)
To: arouhi
Cc: Jakub Kicinski, jiri, vadim.fedorenko, arkadiusz.kubalewski,
ivecera, robh, krzk+dt, conor+dt, cjubran, pabeni,
Oleg.Zadorozhnyi, prabhakar.mahadev-lad.rj, dev, devicetree,
netdev, linux-kernel
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
dpll: sit9531x: allow the device tree to override two board facts
This patch adds two optional device tree overrides to the sit9531x driver.
"sitime,pll-fvco" replaces the VCO frequency derived from DIVN for each
PLL. "sitime,output-pll-map" replaces the output-to-PLL routing that is
normally read from the OUT_MAP registers.
> diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
> index e8d47999f1a97..5316e6bcb338c 100644
> --- a/drivers/dpll/sit9531x/core.c
> +++ b/drivers/dpll/sit9531x/core.c
[ ... ]
> @@ -3238,6 +3241,27 @@ static int sit9531x_out_state_fetch(struct sit9531x_dev *sitdev, u8 index)
>
> sitdev->out[index].state_stale = false;
>
> + /*
> + * DT board-config override: the per-PLL OUTPUT_ENABLE bitmaps
> + * (0x27/0x28) do not unambiguously express output->PLL routing on
> + * every config (overlaps, and some outputs routed outside that
> + * path). When the board supplies an explicit map, trust it.
> + */
> + if (sitdev->out_pll_map_valid) {
> + u8 m = sitdev->out_pll_map[index];
[Severity: Medium]
Which index namespace is out_pll_map[] meant to use?
Here the map is indexed by the driver's packed logical output index. The
register path further down in the same function uses the physical slot:
slot = sitdev->info->clkout_map[index];
The other firmware-facing output code also uses the physical slot.
sit9531x_prop_pin_fwnode_get() in prop.c translates before it matches
output-pins/pin@reg:
id = sitdev->info->clkout_map[id];
sit9531x_prop_pin_label_init() also names package labels by slot:
"OUT%u", sitdev->info->clkout_map[id]);
On SiT95317 the mapping is not contiguous:
static const u8 clkout_map_95317[] = {0, 3, 4, 5, 7, 8, 9, 11};
So map entry 1 describes OUT3 and entry 4 describes OUT7. Seven of the
eight positions don't match OUTn numbering.
The binding text for sitime,output-pll-map only says "for each output
0..11". It doesn't say that SiT95317 entries are packed logical indices.
Suppose a board author numbers the map by OUTn, as pin@reg and the labels
suggest. Could the routing then be applied to the wrong outputs without
any warning?
An 8- or 12-entry map like that passes the value checks in
sit9531x_parse_board_config(). It also passes the length check in
sit9531x_dev_probe(), because the count is not less than num_outputs.
The misrouted out->pll_idx then picks the wrong PLL's Fvco in
sit9531x_get_fvco(). That affects the frequency get, frequency set and
phase adjust paths. The low and high VCO bands are disjoint, so the error
would be large. A 255 entry in the wrong position would also unregister
the wrong pin.
This binding is new in this series, so should the indexing be settled
before DTs ship with it? The map could be indexed by physical slot like
pin@reg, or the binding could document the SiT95317 packing explicitly.
> +
> + if (m < SIT9531X_NUM_PLLS) {
> + out->pll_idx = m;
> + out->routed = true;
> + out->enabled = !muted;
> + } else {
> + out->pll_idx = 0;
> + out->routed = false;
> + out->enabled = false;
> + }
> + return 0;
> + }
[Severity: Low]
This isn't a bug, but the header comment of sit9531x_out_state_fetch() is
now stale. It still says:
* Reads the output PLL association from the PLL page output map
* registers into out->routed / out->pll_idx, ...
With out_pll_map_valid set, pll_idx and routed come from the DT map and
the OUT_MAP_LO/HI registers are never read. The comment for
sit9531x_get_fvco() was updated for its override path in this patch.
Could this one be updated too?
> +
> /*
> * The OUT_MAP_LO/HI bitmaps are indexed by the physical slot the
> * output occupies on the chip, not by the driver's logical output
[ ... ]
> + /*
> + * The override is used verbatim by the divider math, so an
> + * implausible value (units typo, wrong cell count worked
> + * around with zeros) must not silently misprogram DIVO.
> + * Anything outside both VCO bands is dropped with a warning
> + * rather than trusted.
> + */
> + for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
> + u64 f = sitdev->pll_fvco[i];
> +
> + if (f && !sit9531x_fvco_in_band(i, f)) {
> + dev_warn(sitdev->dev,
> + "PLL%c Fvco override %llu Hz is outside the band that PLL runs in, ignoring\n",
> + 'A' + i, f);
> + sitdev->pll_fvco[i] = 0;
> + }
> + }
[Severity: Low]
Should the binding describe the per-PLL range enforced here?
The binding declares sitime,pll-fvco as a 4-item uint64-array. It has no
per-item minimum or maximum and no documented range, and says only that 0
keeps the register-derived value.
This loop zeroes any non-zero entry that fails sit9531x_fvco_in_band().
That check requires 6.875 to 7.8125 GHz for PLLB/PLLD and 4.9152 to
5.89824 GHz for PLLA/PLLC.
A DT can pass dt_binding_check and still have its override dropped at
probe. Examples are a PLLA value in the high band, or a value in the gap
between the bands. The driver then falls back to the DIVN derivation that
the override was meant to replace.
The binding example of 6875000000 for PLLB sits exactly on
SIT9531X_FVCO_HIGHBAND_MIN. It is accepted only because the check is
inclusive.
[ ... ]
> diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
> --- a/drivers/dpll/sit9531x/core.h
> +++ b/drivers/dpll/sit9531x/core.h
> @@ -224,6 +224,7 @@ struct sit9531x_dev {
> u64 pll_fvco[SIT9531X_NUM_PLLS];
> u8 out_pll_map[SIT9531X_MAX_OUTPUTS];
> bool out_pll_map_valid;
> + u8 out_pll_map_count;
>
> /* Inter-PLL synchronization state */
> s8 intsync_src;
[Severity: Low]
This isn't a bug, but the kernel-doc block above struct sit9531x_dev
wasn't updated for the new out_pll_map_count member. Running
scripts/kernel-doc over core.h will warn about the missing
@out_pll_map_count description.
A short description would also help the reader. The name alone doesn't
say what the member counts.
sit9531x_parse_board_config() fills it with the raw length of the
sitime,output-pll-map property. That happens before the variant is
known. The value only means something while out_pll_map_valid is set.
sit9531x_dev_probe() then compares it against info->num_outputs to
decide whether to drop the map.
Could a one-line @out_pll_map_count entry be added that says this?
^ permalink raw reply [flat|nested] 28+ messages in thread