[PATCH net-next v9 11/15] dpll: sit9531x: add support to get and set esync on pins

From: Ali Rouhi

Date: Mon Sep 14 2026 - 20:05:42 EST


From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@xxxxxxxxxxxxxxxxx>

Embedded sync marks a one-pulse-per-second boundary inside a higher-rate
output by widening one pulse. On this device that is the same divider
programming as a frequency change plus the per-output pulse control, so
enabling it sets the output to 1 Hz and disabling it restores the
requested rate.

Only outputs the firmware describes as esync-controllable offer it; on the
rest the operations are refused rather than silently reprogramming an
output whose board wiring does not expect it. The pulse width is fixed at
half the period, which is what the device produces, and the core is told
the supported range rather than left to guess.

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/core.c | 141 ++++++++++++++++++++++++++++++++
drivers/dpll/sit9531x/core.h | 2 +
drivers/dpll/sit9531x/dpll.c | 154 ++++++++++++++++++++++++++++++++++-
drivers/dpll/sit9531x/dpll.h | 3 +-
drivers/dpll/sit9531x/regs.h | 7 ++
5 files changed, 305 insertions(+), 2 deletions(-)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 8d857f1a0c89..46179a9e13c3 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -1654,6 +1654,103 @@ int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
return rc;
}

+/*
+ * sit9531x_output_pulse_write - write an output's PROG0 pulse control
+ *
+ * The caller must already be in the programming state.
+ */
+static int sit9531x_output_pulse_write(struct sit9531x_dev *sitdev, u8 out_idx,
+ u8 pulse_ctrl)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ u8 slot, page, reg;
+
+ slot = info->clkout_map[out_idx];
+ page = (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX) ?
+ SIT9531X_PAGE_OUTSYS1 : SIT9531X_PAGE_OUTSYS0;
+ reg = SIT9531X_OUT_PROG0_BASE +
+ SIT9531X_OUT_PRG_SLOT_STRIDE * (slot % 6);
+
+ return sit9531x_write_u8(sitdev, SIT9531X_REG(page, reg), pulse_ctrl);
+}
+
+/*
+ * sit9531x_output_esync_program - set carrier, marker and enable at once
+ *
+ * Turning embedded sync on means three things to the output system: the
+ * carrier rate, the pulse generator that puts the marker on it, and the
+ * output enable. The device takes them all inside one programming state
+ * -- our validated divider sequence writes a whole register group
+ * that way -- so doing them as three sequences would pay the settling
+ * time three times, with the subsystem's device lock held throughout.
+ *
+ * The phase flush stays after the latch: it aligns the output to the
+ * divider the device is running, not to the one it was asked for.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_output_esync_program(struct sit9531x_dev *sitdev, u8 out_idx,
+ u8 pll_idx, u64 carrier, u8 pulse_ctrl)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ u64 fvco, divo;
+ bool muted;
+ int rc, ret;
+ u8 slot;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ rc = sit9531x_output_divo_calc(sitdev, out_idx, pll_idx, carrier,
+ &fvco, &divo);
+ if (rc)
+ return rc;
+
+ slot = info->clkout_map[out_idx];
+
+ rc = sit9531x_prg_enter(sitdev);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_output_divo_write(sitdev, out_idx, divo);
+ if (!rc)
+ rc = sit9531x_output_pulse_write(sitdev, out_idx, pulse_ctrl);
+ /*
+ * Keep the mute the user asked for. Embedded sync changes what the
+ * output carries, not whether it is driven, so an output muted
+ * through pin-state stays muted.
+ */
+ if (!rc)
+ rc = sit9531x_output_hiz_write(sitdev, slot,
+ !sitdev->out[out_idx].enabled);
+
+ ret = sit9531x_prg_commit(sitdev);
+ if (ret && !rc)
+ rc = ret;
+
+ if (!sit9531x_output_forced_hiz(sitdev, out_idx, &muted))
+ sitdev->out[out_idx].enabled = !muted;
+
+ if (rc)
+ return rc;
+
+ rc = sit9531x_output_phase_flush(sitdev, pll_idx);
+ if (rc)
+ return rc;
+
+ sitdev->out[out_idx].freq = div64_u64(fvco, divo);
+
+ /*
+ * The delay registers count VCO cycles against the output period in
+ * force when they were written, so the carrier change re-times a
+ * phase adjust the same way a frequency set does.
+ */
+ if (sitdev->out[out_idx].phase_adj)
+ return sit9531x_output_phase_adjust_set(sitdev, out_idx,
+ sitdev->out[out_idx].phase_adj);
+
+ return 0;
+}
+
/*
* sit9531x_output_freq_get - read output clock frequency from hardware
* @out_idx: output index (0-N for this chip variant)
@@ -1991,6 +2088,50 @@ int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
return 0;
}

+/*
+ * sit9531x_output_pulse_ctrl_set - program per-output PULSE_CTRL byte
+ * @out_idx: logical output index (translated to chip slot internally)
+ * @pulse_ctrl: 8-bit PULSE_CTRL value (PROG0)
+ *
+ * Writes ODRn_PROG0 on the output page (Page 3 for slots 0..5,
+ * Page 4 for slots 6..11) at offset 0x1B + 16 * (slot % 6).
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_output_pulse_ctrl_set(struct sit9531x_dev *sitdev,
+ u8 out_idx, u8 pulse_ctrl)
+{
+ const struct sit9531x_chip_info *info = sitdev->info;
+ int rc, ret;
+
+ lockdep_assert_held(&sitdev->multiop_lock);
+
+ if (out_idx >= info->num_outputs)
+ return -EINVAL;
+
+ /*
+ * PROG0 lives in the output system, so like the DIVO and
+ * PRG_RST_DELAY writes it only takes effect inside the PRG_CMD
+ * programming state committed to the NVM shadow.
+ */
+ rc = sit9531x_prg_enter(sitdev);
+ if (rc)
+ return rc;
+
+ rc = sit9531x_output_pulse_write(sitdev, out_idx, pulse_ctrl);
+
+ /*
+ * Always leave the PRG_CMD state via prg_commit(), even if the write
+ * failed, 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;
+
+ 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 06df7a91a7c6..1f77ff28ec81 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -281,6 +281,8 @@ int sit9531x_intsync_enable(struct sit9531x_dev *sitdev, u8 src_pll_idx);
int sit9531x_intsync_disable(struct sit9531x_dev *sitdev, u8 src_pll_idx);

/* ---- Output pulse control ---- */
+int sit9531x_output_esync_program(struct sit9531x_dev *sitdev, u8 out_idx,
+ u8 pll_idx, u64 carrier, u8 pulse_ctrl);
int sit9531x_output_pulse_ctrl_set(struct sit9531x_dev *sitdev,
u8 out_idx, u8 pulse_ctrl);

diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index 78034b7f089c..0cad081eb599 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -22,9 +22,21 @@
#include "prop.h"
#include "regs.h"

-#define SIT9531X_ESYNC_FREQ_10MHZ 10000000ULL
+#define SIT9531X_ESYNC_FREQ_1HZ DPLL_PIN_FREQUENCY_1_HZ
+#define SIT9531X_ESYNC_FREQ_10MHZ DPLL_PIN_FREQUENCY_10_MHZ
#define SIT9531X_ESYNC_PULSE_DEFAULT 50

+static const struct dpll_pin_frequency sit9531x_esync_ranges[] = {
+ DPLL_PIN_FREQUENCY(0),
+ DPLL_PIN_FREQUENCY(SIT9531X_ESYNC_FREQ_1HZ),
+};
+
+static bool
+sit9531x_dpll_esync_pin_supported(const struct sit9531x_dpll_pin *dpin)
+{
+ return dpin->esync_control;
+}
+
static bool sit9531x_dpll_is_input_pin(const struct sit9531x_dpll_pin *pin)
{
return pin->dir == DPLL_PIN_DIRECTION_INPUT;
@@ -726,6 +738,12 @@ sit9531x_dpll_output_pin_frequency_set(const struct dpll_pin *pin,
actual_pll = sitdev->out[dpin->id].pll_idx;

mutex_lock(&sitdev->multiop_lock);
+ if (dpin->esync_freq) {
+ mutex_unlock(&sitdev->multiop_lock);
+ NL_SET_ERR_MSG(extack,
+ "Disable embedded sync on this pin before changing frequency");
+ return -EBUSY;
+ }
rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll,
frequency);
mutex_unlock(&sitdev->multiop_lock);
@@ -869,6 +887,138 @@ sit9531x_dpll_output_pin_phase_adjust_set(const struct dpll_pin *pin,
return 0;
}

+static int
+sit9531x_dpll_output_pin_esync_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ struct dpll_pin_esync *esync,
+ struct netlink_ext_ack *extack)
+{
+ struct sit9531x_dpll_pin *dpin = pin_priv;
+ struct sit9531x_dpll *sitdpll = dpll_priv;
+ struct sit9531x_dev *sitdev = sitdpll->dev;
+
+ if (!sit9531x_dpll_esync_pin_supported(dpin))
+ return -EOPNOTSUPP;
+
+ mutex_lock(&sitdev->multiop_lock);
+ esync->range = sit9531x_esync_ranges;
+ esync->range_num = ARRAY_SIZE(sit9531x_esync_ranges);
+ esync->freq = dpin->esync_freq;
+ /*
+ * The hardware PROG0 byte is programmed with the same literal that the
+ * ABI reports as pulse percent for the supported 1 Hz mode. When esync
+ * is disabled, report pulse as zero to avoid implying an active marker.
+ */
+ esync->pulse = dpin->esync_freq ? SIT9531X_ESYNC_PULSE_DEFAULT : 0;
+ mutex_unlock(&sitdev->multiop_lock);
+
+ return 0;
+}
+
+static int
+sit9531x_dpll_output_pin_esync_set(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ u64 freq,
+ 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;
+
+ if (!sit9531x_dpll_esync_pin_supported(dpin)) {
+ NL_SET_ERR_MSG(extack,
+ "Embedded sync not enabled for this pin");
+ return -EOPNOTSUPP;
+ }
+
+ actual_pll = sitdev->out[dpin->id].pll_idx;
+
+ mutex_lock(&sitdev->multiop_lock);
+
+ if (!freq) {
+ u64 prev_freq = dpin->esync_prev_freq;
+ u8 pulse = SIT9531X_ESYNC_PULSE_DEFAULT;
+
+ if (!dpin->esync_freq) {
+ mutex_unlock(&sitdev->multiop_lock);
+ return 0;
+ }
+ rc = sit9531x_output_pulse_ctrl_set(sitdev, dpin->id, 0);
+ if (!rc && prev_freq)
+ rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll, prev_freq);
+ if (rc)
+ sit9531x_output_pulse_ctrl_set(sitdev, dpin->id, pulse);
+ if (!rc)
+ dpin->esync_freq = 0;
+ mutex_unlock(&sitdev->multiop_lock);
+ if (rc)
+ NL_SET_ERR_MSG(extack,
+ "Failed to turn embedded sync off on this output");
+ return rc;
+ }
+
+ if (freq != SIT9531X_ESYNC_FREQ_1HZ) {
+ mutex_unlock(&sitdev->multiop_lock);
+ NL_SET_ERR_MSG(extack,
+ "Only 1 Hz embedded-sync frequency is supported");
+ return -EINVAL;
+ }
+
+ if (dpin->esync_freq == SIT9531X_ESYNC_FREQ_1HZ) {
+ mutex_unlock(&sitdev->multiop_lock);
+ return 0;
+ }
+
+ if (!dpin->esync_freq) {
+ /*
+ * Remember the carrier to restore. The cached rate is zero
+ * for an output whose firmware node lists no frequency and
+ * which has never been set, so read the divider rather than
+ * leave the output parked on the esync carrier at disable.
+ */
+ dpin->esync_prev_freq = sitdev->out[dpin->id].freq;
+ if (!dpin->esync_prev_freq)
+ sit9531x_output_freq_get(sitdev, dpin->id,
+ &dpin->esync_prev_freq);
+ }
+
+ /*
+ * Carrier, marker and enable go into the device in one programming
+ * sequence. Without the pulse generator (PROG0 PULSE_CTRL) the
+ * output would carry 10 MHz and no marker at all;
+ * SIT9531X_ESYNC_PULSE_DEFAULT is the duty esync_get advertises for
+ * the 1 Hz embedded-sync signal.
+ */
+ rc = sit9531x_output_esync_program(sitdev, dpin->id, actual_pll,
+ SIT9531X_ESYNC_FREQ_10MHZ,
+ SIT9531X_ESYNC_PULSE_DEFAULT);
+ if (rc) {
+ sit9531x_output_pulse_ctrl_set(sitdev, dpin->id, 0);
+ if (dpin->esync_prev_freq)
+ sit9531x_output_freq_set(sitdev, dpin->id,
+ actual_pll,
+ dpin->esync_prev_freq);
+ }
+
+ mutex_unlock(&sitdev->multiop_lock);
+
+ if (rc) {
+ NL_SET_ERR_MSG(extack,
+ "Failed to program embedded sync on this output");
+ return rc;
+ }
+
+ dpin->esync_freq = SIT9531X_ESYNC_FREQ_1HZ;
+
+ 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,
@@ -877,6 +1027,8 @@ static const struct dpll_pin_ops sit9531x_dpll_output_pin_ops = {
.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,
+ .esync_get = sit9531x_dpll_output_pin_esync_get,
+ .esync_set = sit9531x_dpll_output_pin_esync_set,
};

const struct dpll_pin_ops *
diff --git a/drivers/dpll/sit9531x/dpll.h b/drivers/dpll/sit9531x/dpll.h
index 901aec238f4f..0b8b24cadcbb 100644
--- a/drivers/dpll/sit9531x/dpll.h
+++ b/drivers/dpll/sit9531x/dpll.h
@@ -34,7 +34,8 @@ struct sit9531x_dpll_pin {
/* in 1/DPLL_PHASE_OFFSET_DIVIDER picosecond units */
s64 phase_offset;
bool esync_control;
- u64 esync_freq; /* 0 == disabled */
+ u64 esync_freq; /* 0 == disabled, 1 Hz == enabled */
+ u64 esync_prev_freq; /* carrier to restore on disable */
};

/* Per-PLL DPLL device state. */
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index f40762f06403..9d515e109feb 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -230,6 +230,13 @@
#define SIT9531X_OUT_PRG_FINE_MAX 7 /* 3-bit field */
#define SIT9531X_OUT_PRG_COARSE_BITS 34

+/*
+ * Per-output pulse-count control byte used in SYSREF / SYNCB modes.
+ * Slot N within a page sits at 0x1B + 16 * (slot % 6). Same page
+ * mapping as PRG_RST_DELAY: slots 0-5 on Page 3, slots 6-11 on Page 4.
+ */
+#define SIT9531X_OUT_PROG0_BASE 0x1B
+
/*
* 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.39.2 (Apple Git-143)