[PATCH v10 03/14] dpll: add basic SiTime SiT9531x support

From: Ali Rouhi

Date: Mon Sep 21 2026 - 16:25:39 EST


From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@xxxxxxxxxxxxxxxxx>

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@xxxxxxxxxx>
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@xxxxxxxxxxxxxxxxx>
Assisted-by: Claude:claude-4-opus [chat]
Signed-off-by: Ali Rouhi <arouhi@xxxxxxxxxx>
---

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@xxxxxxxxxx>
L: netdev@xxxxxxxxxxxxxxx
S: Maintained
F: Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
+F: drivers/dpll/sit9531x/

SL28 CPLD MFD DRIVER
M: Michael Walle <mwalle@xxxxxxxxxx>
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@xxxxxxxxxx>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@xxxxxxxxxxxxxxxxx>
+ *
+ * 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@xxxxxxxxxx>");
+MODULE_AUTHOR("Oleg Zadorozhnyi <Oleg.Zadorozhnyi@xxxxxxxxxxxxxxxxx>");
+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@xxxxxxxxxx>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@xxxxxxxxxxxxxxxxx>
+ *
+ * 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@xxxxxxxxxx>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@xxxxxxxxxxxxxxxxx>
+ */
+
+#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