[PATCH net-next v11 05/13] dpll: sit9531x: read DPLL types and pin properties from system firmware

From: Ali Rouhi

Date: Wed Sep 30 2026 - 19:38:20 EST


From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@xxxxxxxxxxxxxxxxx>

The DPLL core wants a type per device and a property set per pin: a
label, the direction, the capabilities, and the frequencies the pin
supports. None of that can be read from the chip -- which frequencies a
board actually presents on a given input is a board fact -- so they come
from the firmware node, with defaults for a node that does not describe
them.

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

One of the advertised properties is worth naming. An output whose node
lists no supported frequencies is advertised as a continuous range rather
than nothing at all, since those pins do accept a frequency set; a
request inside the range that the integer output divider cannot produce
is refused when it is made.

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

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@xxxxxxxxxxxxxxxxx>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@xxxxxxxxxx>
---
drivers/dpll/sit9531x/Makefile | 2 +-
drivers/dpll/sit9531x/core.h | 68 ++++++
drivers/dpll/sit9531x/prop.c | 427 +++++++++++++++++++++++++++++++++
drivers/dpll/sit9531x/prop.h | 37 +++
4 files changed, 533 insertions(+), 1 deletion(-)
create mode 100644 drivers/dpll/sit9531x/prop.c
create mode 100644 drivers/dpll/sit9531x/prop.h

diff --git a/drivers/dpll/sit9531x/Makefile b/drivers/dpll/sit9531x/Makefile
index a221fe55386a..819af61123f5 100644
--- a/drivers/dpll/sit9531x/Makefile
+++ b/drivers/dpll/sit9531x/Makefile
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only

obj-$(CONFIG_SIT9531X_DPLL) += sit9531x.o
-sit9531x-y := core.o
+sit9531x-y := core.o prop.o
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 76a2632f0ce4..4b4f72c8622b 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -23,7 +23,17 @@

#define SIT9531X_NUM_PLLS 4
#define SIT9531X_MAX_INPUTS 8
+#define SIT9531X_NUM_INPUT_PAIRS (SIT9531X_MAX_INPUTS / 2)
#define SIT9531X_MAX_OUTPUTS 12
+/*
+ * INTSYNC (the inter-PLL sync net) is modeled as two pins. The
+ * destination PLL that locks to INTSYNC sees an input pin
+ * (SIT9531X_INTSYNC_PIN_ID, in the input id namespace after the physical
+ * inputs and the xtal); the source PLL that drives INTSYNC sees an output
+ * pin (SIT9531X_INTSYNC_OUT_PIN_ID, appended after the physical outputs).
+ */
+#define SIT9531X_INTSYNC_PIN_ID (SIT9531X_MAX_INPUTS + 1)
+#define SIT9531X_INTSYNC_OUT_PIN_ID SIT9531X_MAX_OUTPUTS

/*
* struct sit9531x_chip_info - chip variant identification
@@ -41,6 +51,35 @@ struct sit9531x_chip_info {
const u8 *clkout_map;
};

+/*
+ * enum sit9531x_signal_mode - input signal electrical mode
+ * @SIT9531X_MODE_SE: single-ended
+ * @SIT9531X_MODE_DE: differential
+ */
+enum sit9531x_signal_mode {
+ SIT9531X_MODE_SE = 0,
+ SIT9531X_MODE_DE,
+};
+
+/*
+ * struct sit9531x_ref - input reference state
+ * @freq: configured frequency in Hz
+ * @sig_mode: signal mode of the pair this lane belongs to
+ * (detected from CLKINx_INPUT_MODE at probe)
+ */
+struct sit9531x_ref {
+ u64 freq;
+ enum sit9531x_signal_mode sig_mode;
+};
+
+/*
+ * struct sit9531x_out - output state
+ * @freq: configured frequency in Hz
+ */
+struct sit9531x_out {
+ u64 freq;
+};
+
/*
* struct sit9531x_dev - SiT9531x device instance
* @dev: parent device
@@ -48,6 +87,8 @@ struct sit9531x_chip_info {
* @regmap: paged register map
* @info: detected chip variant info
* @multiop_lock: serializes multi-register sequences
+ * @ref: array of input reference states
+ * @out: array of output states
* @xtal_freq: crystal oscillator frequency in Hz
* @reset_gpio: optional reset line (DT "reset-gpios"), NULL if absent
*/
@@ -59,11 +100,38 @@ struct sit9531x_dev {
/* Serializes multi-step register sequences */
struct mutex multiop_lock;

+ /* Hardware state */
+ struct sit9531x_ref ref[SIT9531X_MAX_INPUTS + 1]; /* +1 for xtal */
+ struct sit9531x_out out[SIT9531X_MAX_OUTPUTS];
u32 xtal_freq;

struct gpio_desc *reset_gpio;
};

+/*
+ * Logical input pins are interleaved: even index = P lane, odd
+ * index = N lane of pair index/2 (IN0P, IN0N, IN1P, IN1N, ...).
+ * Index SIT9531X_MAX_INPUTS is the XO input.
+ */
+
+/*
+ * sit9531x_input_pair - get input pair number for a logical input index
+ * @index: logical input pin index
+ */
+static inline u8 sit9531x_input_pair(u8 index)
+{
+ return index >> 1;
+}
+
+/*
+ * sit9531x_input_is_n - check if a logical input index is an N lane
+ * @index: logical input pin index
+ */
+static inline bool sit9531x_input_is_n(u8 index)
+{
+ return index & 1;
+}
+
/*
* sit9531x_pll_page - get register page for PLL index
* @pll_idx: PLL index (0 = PLLA, 3 = PLLD)
diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
new file mode 100644
index 000000000000..3635ea497e41
--- /dev/null
+++ b/drivers/dpll/sit9531x/prop.c
@@ -0,0 +1,427 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * SiTime SiT9531x firmware node property parsing
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@xxxxxxxxxx>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@xxxxxxxxxxxxxxxxx>
+ *
+ * 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", &reg))
+ continue;
+
+ if (id == reg)
+ break;
+ }
+
+ fwnode_handle_put(pins_node);
+
+ props->fwnode = pin_node;
+
+ dev_dbg(sitdev->dev, "Firmware node for %s %sfound\n",
+ props->package_label, pin_node ? "" : "NOT ");
+
+ return pin_node ? 0 : -ENOENT;
+}
+
+/*
+ * sit9531x_pin_props_get - get pin properties for a given pin
+ * @dir: pin direction (INPUT or OUTPUT)
+ * @index: pin index
+ *
+ * Allocates a pin properties structure, generates a package label,
+ * looks up the firmware node if available, and reads optional
+ * properties (label, connection-type, supported-frequencies-hz).
+ *
+ * Call sit9531x_pin_props_put() to free the returned structure.
+ *
+ * Return: pointer to pin properties on success, error pointer on error
+ */
+struct sit9531x_pin_props *
+sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
+ enum dpll_pin_direction dir, u8 index)
+{
+ struct dpll_pin_frequency *ranges;
+ struct sit9531x_pin_props *props;
+ int i, j, num_freqs = 0, rc;
+ u64 *freqs = NULL;
+ const char *type;
+ u64 curr_freq;
+
+ props = kzalloc_obj(*props, GFP_KERNEL);
+ if (!props)
+ return ERR_PTR(-ENOMEM);
+
+ if (dir == DPLL_PIN_DIRECTION_INPUT &&
+ index == SIT9531X_INTSYNC_PIN_ID) {
+ /*
+ * INTSYNC destination pin: a PLL locks to the INTSYNC net as a
+ * reference, so it can be connected.
+ */
+ props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+ props->dpll_props.capabilities =
+ DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+ curr_freq = 0;
+ } else if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+ index == SIT9531X_INTSYNC_OUT_PIN_ID) {
+ /*
+ * INTSYNC source pin: a PLL drives the INTSYNC net. It can be
+ * connected/disconnected but carries no priority (driving the
+ * net is not a reference selection) and no frequency.
+ */
+ props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+ props->dpll_props.capabilities =
+ DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+ curr_freq = 0;
+ } else if (dir == DPLL_PIN_DIRECTION_INPUT &&
+ index == SIT9531X_MAX_INPUTS) {
+ /* The XO reference is fixed: no state or priority control. */
+ props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+ props->dpll_props.capabilities = 0;
+ sitdev->ref[index].freq = sitdev->xtal_freq;
+ curr_freq = sitdev->xtal_freq;
+ } else if (dir == DPLL_PIN_DIRECTION_INPUT) {
+ props->dpll_props.type = DPLL_PIN_TYPE_EXT;
+ props->dpll_props.capabilities =
+ DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+ curr_freq = sitdev->ref[index].freq;
+ } else {
+ /*
+ * A synthesized clock output is an external connection with
+ * no more specific meaning; a board that knows better says
+ * so through the pin's connection-type property below.
+ */
+ props->dpll_props.type = DPLL_PIN_TYPE_EXT;
+ props->dpll_props.capabilities =
+ DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+ curr_freq = sitdev->out[index].freq;
+ }
+
+ /* Generate package label */
+ sit9531x_prop_pin_package_label_set(sitdev, props, dir, index);
+
+ /*
+ * Both INTSYNC pins are internal to the chip and have no board-level
+ * wiring, so they take no properties from the firmware node.
+ */
+ if (dir == DPLL_PIN_DIRECTION_INPUT &&
+ (index == SIT9531X_INTSYNC_PIN_ID ||
+ index == SIT9531X_MAX_INPUTS))
+ goto skip_fwnode_props;
+ if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+ index == SIT9531X_INTSYNC_OUT_PIN_ID)
+ goto skip_fwnode_props;
+
+ rc = sit9531x_prop_pin_fwnode_get(sitdev, props, dir, index);
+ if (rc)
+ goto skip_fwnode_props;
+
+ /* Look for "label" property -> board label */
+ fwnode_property_read_string(props->fwnode, "label",
+ &props->dpll_props.board_label);
+
+ /* Look for "connection-type" property -> pin type enum */
+ if (!fwnode_property_read_string(props->fwnode, "connection-type",
+ &type)) {
+ if (!strcmp(type, "ext"))
+ props->dpll_props.type = DPLL_PIN_TYPE_EXT;
+ else if (!strcmp(type, "gnss"))
+ props->dpll_props.type = DPLL_PIN_TYPE_GNSS;
+ else if (!strcmp(type, "int"))
+ props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+ else if (!strcmp(type, "synce"))
+ props->dpll_props.type = DPLL_PIN_TYPE_SYNCE_ETH_PORT;
+ else if (!strcmp(type, "mux"))
+ props->dpll_props.type = DPLL_PIN_TYPE_MUX;
+ else
+ dev_warn(sitdev->dev,
+ "Unknown pin type '%s'\n", type);
+ }
+
+ num_freqs = fwnode_property_count_u64(props->fwnode,
+ "supported-frequencies-hz");
+ if (num_freqs <= 0) {
+ num_freqs = 0;
+ goto skip_fwnode_props;
+ }
+
+ freqs = kcalloc(num_freqs, sizeof(*freqs), GFP_KERNEL);
+ if (!freqs) {
+ rc = -ENOMEM;
+ goto err_alloc_freqs;
+ }
+
+ rc = fwnode_property_read_u64_array(props->fwnode,
+ "supported-frequencies-hz",
+ freqs, num_freqs);
+ if (rc) {
+ dev_warn(sitdev->dev,
+ "failed to parse supported-frequencies-hz for %s: %d\n",
+ props->package_label, rc);
+ goto err_alloc_ranges;
+ }
+
+ /*
+ * Seed an input's runtime ref->freq with the first DT-listed
+ * supported frequency: for an input the board lists the rate that is
+ * physically wired to it first. An output's current rate is what its
+ * divider produces, not an entry of the list it may be set to.
+ */
+ if (num_freqs > 0 && dir == DPLL_PIN_DIRECTION_INPUT &&
+ index != SIT9531X_MAX_INPUTS)
+ curr_freq = freqs[0];
+
+skip_fwnode_props:
+ /* Neither INTSYNC pin carries a frequency attribute */
+ if (dir == DPLL_PIN_DIRECTION_INPUT &&
+ index == SIT9531X_INTSYNC_PIN_ID)
+ return props;
+ if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+ index == SIT9531X_INTSYNC_OUT_PIN_ID)
+ return props;
+
+ /*
+ * Advertise only concrete values from firmware plus current runtime
+ * value. For outputs without a firmware list, publish one wide range as
+ * an explicit fallback because those pins do support frequency_set.
+ */
+ ranges = kcalloc(num_freqs + 2, sizeof(*ranges), GFP_KERNEL);
+ if (!ranges) {
+ rc = -ENOMEM;
+ goto err_alloc_ranges;
+ }
+
+ /*
+ * Publish the seeded rate only once the pin is certain to be built.
+ * The allocation above is the last thing that can fail, and a call
+ * that reports failure must not leave the device's cached rate
+ * changed behind it.
+ */
+ if (curr_freq) {
+ if (dir == DPLL_PIN_DIRECTION_INPUT)
+ sitdev->ref[index].freq = curr_freq;
+ else
+ sitdev->out[index].freq = curr_freq;
+ }
+
+ j = 0;
+
+ /* Current frequency first, when known. */
+ if (curr_freq) {
+ struct dpll_pin_frequency f = DPLL_PIN_FREQUENCY(curr_freq);
+
+ ranges[j++] = f;
+ }
+
+ for (i = 0; i < num_freqs; i++) {
+ struct dpll_pin_frequency freq = DPLL_PIN_FREQUENCY(freqs[i]);
+
+ if (freqs[i] == curr_freq)
+ continue;
+ ranges[j++] = freq;
+ }
+
+ if (dir == DPLL_PIN_DIRECTION_OUTPUT && num_freqs == 0) {
+ ranges[j].min = 1;
+ ranges[j].max = 1000000000ULL; /* 1 GHz */
+ j++;
+ }
+
+ if (j > 0) {
+ props->dpll_props.freq_supported = ranges;
+ props->dpll_props.freq_supported_num = j;
+ } else {
+ kfree(ranges);
+ props->dpll_props.freq_supported = NULL;
+ props->dpll_props.freq_supported_num = 0;
+ }
+
+ kfree(freqs);
+
+ return props;
+
+err_alloc_ranges:
+ kfree(freqs);
+err_alloc_freqs:
+ fwnode_handle_put(props->fwnode);
+ kfree(props);
+
+ return ERR_PTR(rc);
+}
+
+/*
+ * sit9531x_pin_props_put - release pin properties
+ * @props: pin properties to free
+ */
+void sit9531x_pin_props_put(struct sit9531x_pin_props *props)
+{
+ kfree(props->dpll_props.freq_supported);
+
+ if (props->fwnode)
+ fwnode_handle_put(props->fwnode);
+
+ kfree(props);
+}
+
+/*
+ * sit9531x_prop_dpll_type_get - get DPLL channel type from firmware
+ * @index: DPLL channel index (0-3)
+ *
+ * Reads the "dpll-types" string array property from the firmware node
+ * and returns the corresponding DPLL type enum.
+ *
+ * Return: DPLL type for the given channel (default: DPLL_TYPE_PPS)
+ */
+enum dpll_type
+sit9531x_prop_dpll_type_get(struct sit9531x_dev *sitdev, u8 index)
+{
+ const char *types[SIT9531X_NUM_PLLS];
+ int count;
+
+ count = device_property_read_string_array(sitdev->dev, "dpll-types",
+ types, ARRAY_SIZE(types));
+
+ if (index >= count)
+ return DPLL_TYPE_PPS;
+
+ if (!strcmp(types[index], "pps"))
+ return DPLL_TYPE_PPS;
+ else if (!strcmp(types[index], "eec"))
+ return DPLL_TYPE_EEC;
+
+ dev_warn(sitdev->dev, "Unknown DPLL type '%s', using default\n",
+ types[index]);
+
+ return DPLL_TYPE_PPS;
+}
diff --git a/drivers/dpll/sit9531x/prop.h b/drivers/dpll/sit9531x/prop.h
new file mode 100644
index 000000000000..a65efdfd9ef8
--- /dev/null
+++ b/drivers/dpll/sit9531x/prop.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * SiTime SiT9531x firmware node property parsing
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@xxxxxxxxxx>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@xxxxxxxxxxxxxxxxx>
+ */
+
+#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