[PATCH net-next v15 10/13] dpll: sit9531x: implement output pin state on a DPLL

From: Ali Rouhi

Date: Tue Nov 10 2026 - 15:35:04 EST


From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@xxxxxxxxxxxxxxxxx>

An output's state says whether it carries a signal: CONNECTED while it
drives, DISCONNECTED while muted. Setting it forces the output's pads to
Hi-Z, which leaves the divider alone. Output pins are registered from
this patch on.

A mute forces the slot's three Hi-Z pairs, differential, CMOS OutP and
CMOS OutN, inside the programming state; an un-mute releases the force
and the pads return to the loaded configuration. An output is reported
muted only when both pads are forced off.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@xxxxxxxxxxxxxxxxx>
Signed-off-by: Ali Rouhi <arouhi@xxxxxxxxxx>
---
drivers/dpll/sit9531x/core.c | 247 +++++++++++++++++++++++++++++------
drivers/dpll/sit9531x/core.h | 10 ++
drivers/dpll/sit9531x/dpll.c | 139 +++++++++++++++++++-
3 files changed, 355 insertions(+), 41 deletions(-)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index c7ab33993c28..458de502bc8f 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -459,53 +459,42 @@ static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev,
return 0;
}

-#define SIT9531X_LOOP_LOCK_TRIES 3
-
-/* Close the loops and lock the debug block without committing */
-static void sit9531x_prg_abort(struct sit9531x_dev *sitdev)
+/* Re-read an output's mute state. Caller must hold sitdev->multiop_lock. */
+int sit9531x_output_state_refresh(struct sit9531x_dev *sitdev, u8 out_idx)
{
- int rc = -EIO;
- u8 attempt;
+ bool muted, part;
+ int rc;

- 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);
- }
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ rc = sit9531x_output_forced_hiz(sitdev, out_idx, &muted, &part);
if (rc)
- dev_err(sitdev->dev,
- "output loops left unlocked after a failed entry: %d\n",
- rc);
+ return rc;
+
+ sitdev->out[out_idx].enabled = !muted;
+ sitdev->out[out_idx].part_muted = part;
+ sitdev->out[out_idx].state_stale = false;

- sit9531x_write_u8(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
- SIT9531X_DEBUG_LOCK_VAL);
+ return 0;
}

-/*
- * Unlock the Page 3 debug registers and enter PRG_CMD; output-system writes
- * take effect only in this state.
- */
-static int sit9531x_prg_enter(struct sit9531x_dev *sitdev)
+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_write_u8(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
- SIT9531X_DEBUG_UNLOCK_VAL);
+ rc = sit9531x_read_u8(sitdev, reg, &cur);
if (rc)
return rc;

- rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
- SIT9531X_PRG_CMD_STATE);
- if (rc) {
- sit9531x_prg_abort(sitdev);
- return rc;
- }
+ new_val = set ? (cur | BIT(bit)) : (cur & ~BIT(bit));

- return 0;
+ return sit9531x_write_u8(sitdev, reg, new_val);
}

+#define SIT9531X_LOOP_LOCK_TRIES 3
+
/*
* Update the NVM shadow and re-lock the loops after sit9531x_prg_enter(). The
* sleep is the settling time after loop lock.
@@ -554,6 +543,174 @@ static int sit9531x_prg_commit(struct sit9531x_dev *sitdev)
return 0;
}

+/*
+ * Unlock the Page 3 debug registers and enter PRG_CMD; output-system writes
+ * take effect only in 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) {
+ sit9531x_write_close(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
+ SIT9531X_DEBUG_LOCK_VAL);
+ return rc;
+ }
+
+ rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+ SIT9531X_PRG_CMD_STATE);
+ if (rc) {
+ sit9531x_prg_commit(sitdev);
+ return rc;
+ }
+
+ return 0;
+}
+
+/*
+ * Mute or unmute every Hi-Z pair of a slot. Caller must already be in the
+ * programming state.
+ */
+static int sit9531x_output_hiz_write(struct sit9531x_dev *sitdev, u8 slot,
+ bool mute)
+{
+ u8 mask[SIT9531X_HIZ_PAIRS], state[SIT9531X_HIZ_PAIRS];
+ struct sit9531x_hiz_pair p[SIT9531X_HIZ_PAIRS];
+ int i, rc, undo_rc;
+
+ sit9531x_output_get_hiz_regs(slot, p);
+
+ if (!mute) {
+ for (i = 0; i < SIT9531X_HIZ_PAIRS; i++) {
+ rc = sit9531x_hiz_set_bit(sitdev, p[i].mask, p[i].bit,
+ false);
+ if (rc)
+ return rc;
+ }
+ return 0;
+ }
+
+ for (i = 0; i < SIT9531X_HIZ_PAIRS; i++) {
+ rc = sit9531x_read_u8(sitdev, p[i].state, &state[i]);
+ if (rc)
+ return rc;
+ rc = sit9531x_read_u8(sitdev, p[i].mask, &mask[i]);
+ if (rc)
+ return rc;
+ }
+
+ /*
+ * STATE before MASK: enabling the override first can drive the pad for
+ * one I2C transfer.
+ */
+ for (i = 0; i < SIT9531X_HIZ_PAIRS; i++) {
+ rc = sit9531x_hiz_set_bit(sitdev, p[i].state, p[i].bit, false);
+ if (!rc)
+ rc = sit9531x_hiz_set_bit(sitdev, p[i].mask, p[i].bit,
+ true);
+ if (rc)
+ break;
+ }
+ if (!rc)
+ return 0;
+
+ for (; i >= 0; i--) {
+ u8 bit = BIT(p[i].bit);
+
+ undo_rc = sit9531x_hiz_set_bit(sitdev, p[i].mask, p[i].bit,
+ mask[i] & bit);
+ if (!undo_rc)
+ undo_rc = sit9531x_hiz_set_bit(sitdev, p[i].state,
+ p[i].bit,
+ state[i] & bit);
+ if (undo_rc)
+ dev_err(sitdev->dev,
+ "slot%u: Hi-Z override left part applied (%d)\n",
+ slot, undo_rc);
+ }
+
+ return rc;
+}
+
+/* Caller must hold sitdev->multiop_lock. */
+int sit9531x_output_disable(struct sit9531x_dev *sitdev, u8 index)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ int rc, ret, state_rc;
+ bool muted, part;
+ u8 slot;
+
+ 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);
+
+ ret = sit9531x_prg_commit(sitdev);
+ if (ret && !rc)
+ rc = ret;
+
+ /* Read back the Hi-Z state */
+ state_rc = sit9531x_output_forced_hiz(sitdev, index, &muted, &part);
+ if (!state_rc) {
+ sitdev->out[index].enabled = !muted;
+ sitdev->out[index].part_muted = part;
+ sitdev->out[index].state_stale = false;
+ } else {
+ sitdev->out[index].state_stale = true;
+ if (!rc)
+ rc = state_rc;
+ }
+
+ return rc;
+}
+
+/* Caller must hold sitdev->multiop_lock. */
+int sit9531x_output_enable(struct sit9531x_dev *sitdev, u8 index)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ int rc, ret, state_rc;
+ bool muted, part;
+ u8 slot;
+
+ 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);
+
+ ret = sit9531x_prg_commit(sitdev);
+ if (ret && !rc)
+ rc = ret;
+
+ state_rc = sit9531x_output_forced_hiz(sitdev, index, &muted, &part);
+ if (!state_rc) {
+ sitdev->out[index].enabled = !muted;
+ sitdev->out[index].part_muted = part;
+ sitdev->out[index].state_stale = false;
+ } else {
+ sitdev->out[index].state_stale = true;
+ if (!rc)
+ rc = state_rc;
+ }
+
+ return rc;
+}
+
/*
* Procedure:
* 1. Force PLL into holdover (PLL page reg 0x6F bit 4)
@@ -2084,6 +2241,7 @@ static int sit9531x_out_state_fetch(struct sit9531x_dev *sitdev, u8 index)
return rc;

sitdev->out[index].part_muted = part;
+ sitdev->out[index].state_stale = false;

/*
* Each PLL page holds a 12-bit output-enable mask: OUT_MAP_LO bits 0-7
@@ -2854,7 +3012,7 @@ static void sit9531x_dpll_pin_unregister(struct sit9531x_dpll_pin *pin)
* Clear before unregistering: callbacks test it under the DPLL device
* lock, which dpll_pin_unregister() also takes.
*/
- pin->dpll_pin = NULL;
+ WRITE_ONCE(pin->dpll_pin, NULL);

dpll_pin_unregister(sitdpll->dpll_dev, dpll_pin, ops, pin);
dpll_pin_put(dpll_pin, &pin->tracker);
@@ -2890,20 +3048,33 @@ static void sit9531x_dpll_pins_unregister(struct sit9531x_dpll *sitdpll)
}
}

-/* Check if the pin class has a state callback, which the DPLL core requires */
+/*
+ * An output is registered when routed to this DPLL, whether or not it is
+ * driving.
+ */
static bool sit9531x_dpll_pin_is_registrable(struct sit9531x_dpll *sitdpll,
enum dpll_pin_direction dir,
u8 index)
{
struct sit9531x_dev *sitdev = sitdpll->dev;

- if (dir != DPLL_PIN_DIRECTION_INPUT)
+ 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;
}

static int sit9531x_dpll_pins_register(struct sit9531x_dpll *sitdpll)
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 6b50b5b64b75..4f3f74ac8d7c 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -101,6 +101,9 @@ struct sit9531x_ref {
* @freq: output frequency in Hz
* @enabled: output is driving, not forced into Hi-Z
* @cmos: single-ended; the CMOS pad Hi-Z pairs apply, not the differential one
+ * @state_stale: cached mute state must be read back before it is reported
+ * @ntf_pending: pin change not yet notified; the poll notifies once the state
+ * is read
* @part_muted: single-ended with one pad forced and the other not; reported
* as driven. The driver's mute forces both pads.
* @routed: mapped to @pll_idx by the initial configuration; an unrouted
@@ -113,6 +116,8 @@ struct sit9531x_out {
u64 freq;
bool enabled;
bool cmos;
+ bool state_stale;
+ bool ntf_pending;
bool part_muted;
bool routed;
u8 pll_idx;
@@ -281,6 +286,11 @@ int sit9531x_input_prio_remove(struct sit9531x_dev *sitdev, u8 pll_idx,
int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
u8 input_idx);

+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);
+
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,
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index a450a81d02f4..a32d37a396b1 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -184,8 +184,9 @@ const struct dpll_device_ops sit9531x_dpll_device_ops = {
* NO_SIGNAL on loss of signal; QUAL_FAILED on frequency drift; STANDBY
* otherwise. The routed reference is used because on loss of signal the device
* falls back to another listed source and leaves the selection naming the lost
- * one. On manual input select, the routed input is CONNECTED. The XO pin is
- * always CONNECTED.
+ * one. On manual input select, the routed input is CONNECTED. An output is
+ * CONNECTED while driven and DISCONNECTED while muted; SELECTABLE is refused.
+ * The XO pin is always CONNECTED.
*/

/* Caller must hold sitdev->multiop_lock. */
@@ -638,10 +639,114 @@ sit9531x_dpll_output_pin_frequency_set(const struct dpll_pin *pin,
return rc;
}

+/*
+ * DISCONNECTED while the output is forced to Hi-Z, by a mute request or by the
+ * loaded profile.
+ */
+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 = 0;
+
+ mutex_lock(&sitdev->multiop_lock);
+ out = sit9531x_out_state_get(sitdev, dpin->id);
+ if (out->state_stale)
+ rc = sit9531x_output_state_refresh(sitdev, dpin->id);
+ if (!rc)
+ *state = out->enabled ? DPLL_PIN_STATE_CONNECTED
+ : DPLL_PIN_STATE_DISCONNECTED;
+ mutex_unlock(&sitdev->multiop_lock);
+ if (rc)
+ NL_SET_ERR_MSG(extack,
+ "Output mute state could not be read back");
+
+ return rc;
+}
+
+/*
+ * Force Hi-Z through the slot's Hi-Z force/state register pairs: differential,
+ * CMOS OutP and CMOS OutN.
+ */
+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)
+{
+ bool enable = state == DPLL_PIN_STATE_CONNECTED;
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+ bool was_enabled, changed;
+ int rc;
+
+ /* An output is routed explicitly, not a selection candidate */
+ if (!enable && state != DPLL_PIN_STATE_DISCONNECTED)
+ return -EINVAL;
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ /*
+ * Skip a request for the current state: the programming sequence
+ * unlocks the output loops for its 100 ms commit. One CMOS pad forced
+ * and the other not is neither state.
+ */
+ if (!sit9531x_output_state_refresh(sitdev, dpin->id) &&
+ sitdev->out[dpin->id].enabled == enable &&
+ !sitdev->out[dpin->id].part_muted) {
+ mutex_unlock(&sitdev->multiop_lock);
+ return 0;
+ }
+
+ was_enabled = sitdev->out[dpin->id].enabled;
+
+ if (enable)
+ rc = sit9531x_output_enable(sitdev, dpin->id);
+ else
+ rc = sit9531x_output_disable(sitdev, dpin->id);
+
+ changed = sitdev->out[dpin->id].enabled != was_enabled ||
+ sitdev->out[dpin->id].state_stale;
+
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc) {
+ struct dpll_pin *dpll_pin;
+
+ NL_SET_ERR_MSG(extack, "Failed to set output pin state");
+ /*
+ * The core notifies only a request that succeeded. Teardown
+ * clears dpll_pin without the core's lock, which is held here;
+ * read it once, as a set handle stays valid until this returns.
+ */
+ dpll_pin = READ_ONCE(dpin->dpll_pin);
+ if (changed && dpll_pin && __dpll_pin_change_ntf(dpll_pin)) {
+ mutex_lock(&sitdev->multiop_lock);
+ sitdev->out[dpin->id].ntf_pending = true;
+ mutex_unlock(&sitdev->multiop_lock);
+ }
+ }
+
+ 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 *
@@ -703,6 +808,31 @@ void sit9531x_dpll_pin_baseline(struct sit9531x_dpll_pin *pin)
&pin->operstate, &pin->prio);
}

+/*
+ * Re-read an output with a pending change notification; return true once the
+ * read succeeds. Caller must hold sitdev->multiop_lock.
+ */
+static bool
+sit9531x_dpll_output_ntf_pending(struct sit9531x_dev *sitdev,
+ const struct sit9531x_dpll_pin *pin)
+{
+ struct sit9531x_out *out;
+
+ if (sit9531x_dpll_is_input_pin(pin) ||
+ pin->id >= sitdev->info->num_outputs)
+ return false;
+
+ out = &sitdev->out[pin->id];
+ if (!out->ntf_pending)
+ return false;
+ if (out->state_stale && sit9531x_output_state_refresh(sitdev, pin->id))
+ return false;
+
+ out->ntf_pending = false;
+
+ return true;
+}
+
/*
* Send notifications for changed lock status and pin state; called from
* sit9531x_dev_periodic_work().
@@ -744,8 +874,11 @@ void sit9531x_dpll_changes_check(struct sit9531x_dpll *sitdpll)
bool changed;
u8 prio;

- if (!sit9531x_dpll_is_selection_pin(pin))
+ if (!sit9531x_dpll_is_selection_pin(pin)) {
+ pin->ntf_pending |=
+ sit9531x_dpll_output_ntf_pending(sitdev, pin);
continue;
+ }

sit9531x_dpll_pin_snapshot(sitdpll, pin, &state, &operstate,
&prio);
--
2.39.2 (Apple Git-143)