* [PATCH net-next 1/6] dpll: zl3073x: notify sibling pin when shared output config changes
2026-09-28 18:55 [PATCH net-next 0/6] dpll: zl3073x: add PTP periodic output support Ivan Vecera
@ 2026-09-28 18:55 ` Ivan Vecera
2026-09-28 18:55 ` [PATCH net-next 2/6] dpll: zl3073x: add GPO support for output pins Ivan Vecera
` (4 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Ivan Vecera @ 2026-09-28 18:55 UTC (permalink / raw)
To: netdev
Cc: Chris du Quesnay, Arkadiusz Kubalewski, Jakub Kicinski,
Jiri Pirko, Min Li, Paolo Abeni, Petr Oros, Richard Cochran,
Vadim Fedorenko, linux-kernel
Each zl3073x output has a P-pin and an N-pin that share a single HW
output and, outside N-pin divide mode, share the output's divisor,
clock type, esync period/width and phase compensation registers.
Changing one of these settings through one pin's dpll_pin therefore
also changes the other (sibling) pin's effective configuration, but
only the pin the change was requested on gets a dpll_pin_change_ntf()
notification - userspace listening on the sibling pin is never told
its frequency, esync configuration or phase adjustment changed.
Add zl3073x_dpll_output_pin_sibling_get() to look up the other pin of
an output pair, and use it in frequency_set() (for the non-N-divided
signal formats, where the output divisor is shared), esync_set() and
phase_adjust_set() to notify the sibling pin, if it is registered,
whenever the shared HW state actually changes.
Tested-by: Chris du Quesnay <Chris.duQuesnay@microchip.com>
Signed-off-by: Ivan Vecera <ivecera@redhat.com>
---
drivers/dpll/zl3073x/dpll.c | 144 ++++++++++++++++++++++++++++--------
1 file changed, 115 insertions(+), 29 deletions(-)
diff --git a/drivers/dpll/zl3073x/dpll.c b/drivers/dpll/zl3073x/dpll.c
index f2e137475b40ff..2c6de4dab8b4ad 100644
--- a/drivers/dpll/zl3073x/dpll.c
+++ b/drivers/dpll/zl3073x/dpll.c
@@ -132,6 +132,30 @@ zl3073x_dpll_pin_get_by_ref(struct zl3073x_dpll *zldpll, u8 ref_id)
return NULL;
}
+/**
+ * zl3073x_dpll_output_pin_sibling_get - get the other pin of an output pair
+ * @pin: output pin whose sibling is sought
+ *
+ * Output pin ids are allocated in P/N pairs (P even, N odd) that share a
+ * single HW output. Looks up the other pin of the pair, if it is
+ * registered as a dpll_pin on this DPLL.
+ *
+ * Return: pointer to sibling pin, or NULL if it is not registered
+ */
+static struct zl3073x_dpll_pin *
+zl3073x_dpll_output_pin_sibling_get(struct zl3073x_dpll_pin *pin)
+{
+ struct zl3073x_dpll_pin *sibling;
+
+ list_for_each_entry(sibling, &pin->dpll->pins, list) {
+ if (!zl3073x_dpll_is_input_pin(sibling) &&
+ sibling->id == (pin->id ^ 1))
+ return sibling;
+ }
+
+ return NULL;
+}
+
static struct zl3073x_dpll_pin *
zl3073x_dpll_nco_pin_get(struct zl3073x_dpll *zldpll)
{
@@ -910,11 +934,13 @@ zl3073x_dpll_output_pin_esync_set(const struct dpll_pin *dpll_pin,
struct zl3073x_dev *zldev = zldpll->dev;
struct zl3073x_dpll_pin *pin = pin_priv;
const struct zl3073x_synth *synth;
+ struct zl3073x_dpll_pin *sibling;
struct zl3073x_out out;
u32 synth_freq;
u8 out_id;
+ int rc;
- guard(mutex)(&zldpll->lock);
+ mutex_lock(&zldpll->lock);
out_id = zl3073x_output_pin_out_get(pin->id);
out = *zl3073x_out_state_get(zldev, out_id);
@@ -923,8 +949,10 @@ zl3073x_dpll_output_pin_esync_set(const struct dpll_pin *dpll_pin,
* for N-division is also used for the esync divider so both cannot
* be used.
*/
- if (zl3073x_out_is_ndiv(&out))
- return -EOPNOTSUPP;
+ if (zl3073x_out_is_ndiv(&out)) {
+ rc = -EOPNOTSUPP;
+ goto unlock;
+ }
/* Update clock type in output mode */
if (freq)
@@ -934,27 +962,44 @@ zl3073x_dpll_output_pin_esync_set(const struct dpll_pin *dpll_pin,
zl3073x_out_clock_type_set(&out,
ZL_OUTPUT_MODE_CLOCK_TYPE_NORMAL);
- /* If esync is being disabled just write mailbox and finish */
- if (!freq)
- return zl3073x_out_state_set(zldev, out_id, &out);
+ if (freq) {
+ /* Get attached synth frequency */
+ synth = zl3073x_synth_state_get(zldev,
+ zl3073x_out_synth_get(&out));
+ synth_freq = zl3073x_synth_freq_get(synth);
- /* Get attached synth frequency */
- synth = zl3073x_synth_state_get(zldev, zl3073x_out_synth_get(&out));
- synth_freq = zl3073x_synth_freq_get(synth);
+ /* Compute and update esync period */
+ out.esync_n_period = synth_freq / (u32)freq / out.div;
- /* Compute and update esync period */
- out.esync_n_period = synth_freq / (u32)freq / out.div;
+ /* Half of the period in units of 1/2 synth cycle can be
+ * represented by the output_div. To get the supported esync
+ * pulse width of 25% of the period the output_div can just
+ * be divided by 2. Note that this assumes that output_div
+ * is even, otherwise some resolution will be lost.
+ */
+ out.esync_n_width = out.div / 2;
+ }
- /* Half of the period in units of 1/2 synth cycle can be represented by
- * the output_div. To get the supported esync pulse width of 25% of the
- * period the output_div can just be divided by 2. Note that this
- * assumes that output_div is even, otherwise some resolution will be
- * lost.
+ /* Commit output configuration */
+ rc = zl3073x_out_state_set(zldev, out_id, &out);
+ if (rc)
+ goto unlock;
+
+ /* The clock type, esync period and esync width are all shared by
+ * both pins of the output pair, so the sibling pin's esync
+ * configuration changes too and userspace has to be notified.
*/
- out.esync_n_width = out.div / 2;
+ sibling = zl3073x_dpll_output_pin_sibling_get(pin);
- /* Commit output configuration */
- return zl3073x_out_state_set(zldev, out_id, &out);
+ mutex_unlock(&zldpll->lock);
+
+ if (sibling)
+ __dpll_pin_change_ntf(sibling->dpll_pin);
+
+ return 0;
+unlock:
+ mutex_unlock(&zldpll->lock);
+ return rc;
}
static int
@@ -984,12 +1029,14 @@ zl3073x_dpll_output_pin_frequency_set(const struct dpll_pin *dpll_pin,
struct zl3073x_dpll *zldpll = dpll_priv;
struct zl3073x_dev *zldev = zldpll->dev;
struct zl3073x_dpll_pin *pin = pin_priv;
+ struct zl3073x_dpll_pin *sibling = NULL;
const struct zl3073x_synth *synth;
u32 new_div, synth_freq;
struct zl3073x_out out;
u8 out_id;
+ int rc;
- guard(mutex)(&zldpll->lock);
+ mutex_lock(&zldpll->lock);
out_id = zl3073x_output_pin_out_get(pin->id);
out = *zl3073x_out_state_get(zldev, out_id);
@@ -1002,7 +1049,8 @@ zl3073x_dpll_output_pin_frequency_set(const struct dpll_pin *dpll_pin,
/* Check signal format */
if (!zl3073x_out_is_ndiv(&out)) {
/* For non N-divided signal formats the frequency is computed
- * as division of synth frequency and output divisor.
+ * as division of synth frequency and output divisor, which
+ * is shared by both pins of the output pair.
*/
out.div = new_div;
@@ -1010,7 +1058,16 @@ zl3073x_dpll_output_pin_frequency_set(const struct dpll_pin *dpll_pin,
out.width = new_div;
/* Commit output configuration */
- return zl3073x_out_state_set(zldev, out_id, &out);
+ rc = zl3073x_out_state_set(zldev, out_id, &out);
+ if (rc)
+ goto unlock;
+
+ /* The other pin's frequency changed too - it has to be
+ * notified about the change.
+ */
+ sibling = zl3073x_dpll_output_pin_sibling_get(pin);
+
+ goto unlock;
}
if (zl3073x_dpll_is_p_pin(pin)) {
@@ -1022,8 +1079,10 @@ zl3073x_dpll_output_pin_frequency_set(const struct dpll_pin *dpll_pin,
* Update divisor for N-pin to keep N-pin frequency.
*/
out.esync_n_period = (out.esync_n_period * out.div) / new_div;
- if (!out.esync_n_period)
- return -EINVAL;
+ if (!out.esync_n_period) {
+ rc = -EINVAL;
+ goto unlock;
+ }
/* Update the output divisor */
out.div = new_div;
@@ -1039,15 +1098,24 @@ zl3073x_dpll_output_pin_frequency_set(const struct dpll_pin *dpll_pin,
* Update divisor for N-pin
*/
out.esync_n_period = div64_u64(synth_freq, frequency * out.div);
- if (!out.esync_n_period)
- return -EINVAL;
+ if (!out.esync_n_period) {
+ rc = -EINVAL;
+ goto unlock;
+ }
}
/* For 50/50 duty cycle the divisor is equal to width */
out.esync_n_width = out.esync_n_period;
/* Commit output configuration */
- return zl3073x_out_state_set(zldev, out_id, &out);
+ rc = zl3073x_out_state_set(zldev, out_id, &out);
+unlock:
+ mutex_unlock(&zldpll->lock);
+
+ if (sibling)
+ __dpll_pin_change_ntf(sibling->dpll_pin);
+
+ return rc;
}
static int
@@ -1086,10 +1154,12 @@ zl3073x_dpll_output_pin_phase_adjust_set(const struct dpll_pin *dpll_pin,
struct zl3073x_dpll *zldpll = dpll_priv;
struct zl3073x_dev *zldev = zldpll->dev;
struct zl3073x_dpll_pin *pin = pin_priv;
+ struct zl3073x_dpll_pin *sibling;
struct zl3073x_out out;
u8 out_id;
+ int rc;
- guard(mutex)(&zldpll->lock);
+ mutex_lock(&zldpll->lock);
out_id = zl3073x_output_pin_out_get(pin->id);
out = *zl3073x_out_state_get(zldev, out_id);
@@ -1098,7 +1168,23 @@ zl3073x_dpll_output_pin_phase_adjust_set(const struct dpll_pin *dpll_pin,
out.phase_comp = phase_adjust / pin->phase_gran;
/* Update output configuration from mailbox */
- return zl3073x_out_state_set(zldev, out_id, &out);
+ rc = zl3073x_out_state_set(zldev, out_id, &out);
+ if (rc) {
+ mutex_unlock(&zldpll->lock);
+ return rc;
+ }
+
+ /* The phase compensation register is shared by both pins of the
+ * output pair, so the sibling pin's phase adjustment changes too.
+ */
+ sibling = zl3073x_dpll_output_pin_sibling_get(pin);
+
+ mutex_unlock(&zldpll->lock);
+
+ if (sibling)
+ __dpll_pin_change_ntf(sibling->dpll_pin);
+
+ return 0;
}
static int
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [PATCH net-next 2/6] dpll: zl3073x: add GPO support for output pins
2026-09-28 18:55 [PATCH net-next 0/6] dpll: zl3073x: add PTP periodic output support Ivan Vecera
2026-09-28 18:55 ` [PATCH net-next 1/6] dpll: zl3073x: notify sibling pin when shared output config changes Ivan Vecera
@ 2026-09-28 18:55 ` Ivan Vecera
2026-09-28 18:55 ` [PATCH net-next 3/6] dpll: zl3073x: allow enabling/disabling " Ivan Vecera
` (3 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Ivan Vecera @ 2026-09-28 18:55 UTC (permalink / raw)
To: netdev
Cc: Chris du Quesnay, Arkadiusz Kubalewski, Jakub Kicinski,
Jiri Pirko, Min Li, Paolo Abeni, Petr Oros, Richard Cochran,
Vadim Fedorenko, linux-kernel
Add the GPO override registers for CMOS output pins: the per-output
output_gpo_en and output_gpo_config_out_p/out_n mailbox fields and the
direct gpo_out_x bitmask registers selecting the static value of an
overridden pin. zl3073x_dev_gpo_set() sets that value for a given GPO
channel and zl3073x_out_pin_func_get()/_set() report and configure the
function (clock or GPO mode) of an individual output pin.
Add the output_ctrl_x stop bits and zl3073x_out_stop()/_start() to
request a clean, edge-aligned stop or restart of an output. Move ctrl
into the cfg struct_group and let zl3073x_out_state_set() write it
directly, since it is no longer invariant and is not part of the
output mailbox.
Tested-by: Chris du Quesnay <Chris.duQuesnay@microchip.com>
Signed-off-by: Ivan Vecera <ivecera@redhat.com>
---
drivers/dpll/zl3073x/core.c | 39 ++++++++++
drivers/dpll/zl3073x/core.h | 1 +
drivers/dpll/zl3073x/out.c | 50 ++++++++++---
drivers/dpll/zl3073x/out.h | 138 +++++++++++++++++++++++++++++++++++-
drivers/dpll/zl3073x/regs.h | 20 ++++++
5 files changed, 236 insertions(+), 12 deletions(-)
diff --git a/drivers/dpll/zl3073x/core.c b/drivers/dpll/zl3073x/core.c
index 230df08e27cd19..7386932df0327f 100644
--- a/drivers/dpll/zl3073x/core.c
+++ b/drivers/dpll/zl3073x/core.c
@@ -628,6 +628,45 @@ int zl3073x_ref_phase_offsets_update(struct zl3073x_dev *zldev, int channel)
ZL_POLL_PHASE_ERR_TIMEOUT_US);
}
+/**
+ * zl3073x_dev_gpo_set - set the static value driven by a GPO channel
+ * @zldev: pointer to zl3073x_dev structure
+ * @gpo: GPO channel index (2 * output index for the P-pin, +1 for the
+ * N-pin)
+ * @value: value to drive when the channel is GPO-overridden
+ *
+ * The gpo_out_x registers are direct, multi-channel bitmask registers
+ * shared by all outputs, so the read-modify-write is serialized against
+ * concurrent updates to other channels via multiop_lock.
+ *
+ * Return: 0 on success, <0 on error
+ */
+int zl3073x_dev_gpo_set(struct zl3073x_dev *zldev, u8 gpo, bool value)
+{
+ unsigned int reg;
+ u8 bit, val;
+ int rc;
+
+ if (gpo >= ZL3073X_NUM_OUTPUT_PINS)
+ return -EINVAL;
+
+ reg = ZL_REG_GPO_OUT(gpo / 8);
+ bit = gpo % 8;
+
+ guard(mutex)(&zldev->multiop_lock);
+
+ rc = zl3073x_read_u8(zldev, reg, &val);
+ if (rc)
+ return rc;
+
+ if (value)
+ val |= BIT(bit);
+ else
+ val &= ~BIT(bit);
+
+ return zl3073x_write_u8(zldev, reg, val);
+}
+
/**
* zl3073x_ref_freq_meas_latch - latch reference frequency measurements
* @zldev: pointer to zl3073x_dev structure
diff --git a/drivers/dpll/zl3073x/core.h b/drivers/dpll/zl3073x/core.h
index 67c10e2595118c..e7064f2958fa5f 100644
--- a/drivers/dpll/zl3073x/core.h
+++ b/drivers/dpll/zl3073x/core.h
@@ -167,6 +167,7 @@ int zl3073x_write_hwreg_seq(struct zl3073x_dev *zldev,
*****************/
int zl3073x_ref_phase_offsets_update(struct zl3073x_dev *zldev, int channel);
+int zl3073x_dev_gpo_set(struct zl3073x_dev *zldev, u8 gpo, bool value);
/**
* zl3073x_dev_is_ref_phase_comp_32bit - check ref phase comp register size
diff --git a/drivers/dpll/zl3073x/out.c b/drivers/dpll/zl3073x/out.c
index 410d15b96d0bf0..82564045890db0 100644
--- a/drivers/dpll/zl3073x/out.c
+++ b/drivers/dpll/zl3073x/out.c
@@ -85,8 +85,22 @@ int zl3073x_out_state_fetch(struct zl3073x_dev *zldev, u8 index)
if (rc)
return rc;
- return zl3073x_read_u32(zldev, ZL_REG_OUTPUT_PHASE_COMP,
- &out->phase_comp);
+ rc = zl3073x_read_u32(zldev, ZL_REG_OUTPUT_PHASE_COMP,
+ &out->phase_comp);
+ if (rc)
+ return rc;
+
+ rc = zl3073x_read_u8(zldev, ZL_REG_OUTPUT_GPO_EN, &out->gpo_en);
+ if (rc)
+ return rc;
+
+ rc = zl3073x_read_u8(zldev, ZL_REG_OUTPUT_GPO_CONFIG_OUT_P,
+ &out->gpo_config_p);
+ if (rc)
+ return rc;
+
+ return zl3073x_read_u8(zldev, ZL_REG_OUTPUT_GPO_CONFIG_OUT_N,
+ &out->gpo_config_n);
}
/**
@@ -108,11 +122,12 @@ const struct zl3073x_out *zl3073x_out_state_get(struct zl3073x_dev *zldev,
* @index: output index to set state for
* @out: desired output state
*
- * Validates that invariant fields have not been modified, skips the HW
- * write if the mutable configuration is unchanged, and otherwise writes
- * only the changed cfg fields to hardware via the mailbox interface.
+ * Skips the HW write if the configuration is unchanged, writes ctrl
+ * directly to the output_ctrl_x register if it differs (it is not part
+ * of the output mailbox), and otherwise writes only the changed cfg
+ * fields to hardware via the mailbox interface.
*
- * Return: 0 on success, -EINVAL if invariants changed, <0 on HW error
+ * Return: 0 on success, <0 on HW error
*/
int zl3073x_out_state_set(struct zl3073x_dev *zldev, u8 index,
const struct zl3073x_out *out)
@@ -120,11 +135,17 @@ int zl3073x_out_state_set(struct zl3073x_dev *zldev, u8 index,
struct zl3073x_out *dout = &zldev->out[index];
int rc;
- /* Reject attempts to change invariant fields (set at fetch only) */
- if (WARN_ON(memcmp(&dout->inv, &out->inv, sizeof(out->inv))))
- return -EINVAL;
+ /* ctrl is a direct register, independent of the output mailbox */
+ if (dout->ctrl != out->ctrl) {
+ rc = zl3073x_write_u8(zldev, ZL_REG_OUTPUT_CTRL(index),
+ out->ctrl);
+ if (rc)
+ return rc;
+
+ dout->ctrl = out->ctrl;
+ }
- /* Skip HW write if configuration hasn't changed */
+ /* Skip the mailbox commit if nothing else has changed */
if (!memcmp(&dout->cfg, &out->cfg, sizeof(out->cfg)))
return 0;
@@ -152,6 +173,15 @@ int zl3073x_out_state_set(struct zl3073x_dev *zldev, u8 index,
if (!rc && dout->phase_comp != out->phase_comp)
rc = zl3073x_write_u32(zldev, ZL_REG_OUTPUT_PHASE_COMP,
out->phase_comp);
+ if (!rc && dout->gpo_en != out->gpo_en)
+ rc = zl3073x_write_u8(zldev, ZL_REG_OUTPUT_GPO_EN,
+ out->gpo_en);
+ if (!rc && dout->gpo_config_p != out->gpo_config_p)
+ rc = zl3073x_write_u8(zldev, ZL_REG_OUTPUT_GPO_CONFIG_OUT_P,
+ out->gpo_config_p);
+ if (!rc && dout->gpo_config_n != out->gpo_config_n)
+ rc = zl3073x_write_u8(zldev, ZL_REG_OUTPUT_GPO_CONFIG_OUT_N,
+ out->gpo_config_n);
if (rc)
return rc;
diff --git a/drivers/dpll/zl3073x/out.h b/drivers/dpll/zl3073x/out.h
index 660889c57bffab..66a8432c00dfd0 100644
--- a/drivers/dpll/zl3073x/out.h
+++ b/drivers/dpll/zl3073x/out.h
@@ -19,6 +19,9 @@ struct zl3073x_dev;
* @esync_n_width: embedded sync or n-pin pulse width
* @phase_comp: phase compensation
* @mode: output mode
+ * @gpo_en: GPO override enable for the P-pin and N-pin
+ * @gpo_config_p: GPO mode configuration for the P-pin
+ * @gpo_config_n: GPO mode configuration for the N-pin
* @ctrl: output control
*/
struct zl3073x_out {
@@ -29,8 +32,9 @@ struct zl3073x_out {
u32 esync_n_width;
s32 phase_comp;
u8 mode;
- );
- struct_group(inv, /* Invariants */
+ u8 gpo_en;
+ u8 gpo_config_p;
+ u8 gpo_config_n;
u8 ctrl;
);
};
@@ -106,6 +110,136 @@ static inline bool zl3073x_out_is_enabled(const struct zl3073x_out *out)
return !!FIELD_GET(ZL_OUTPUT_CTRL_EN, out->ctrl);
}
+/**
+ * zl3073x_out_is_stopped - check if the given output is stopped
+ * @out: pointer to out state
+ *
+ * Return: true if output clock is stopped, false if it is running
+ */
+static inline bool zl3073x_out_is_stopped(const struct zl3073x_out *out)
+{
+ return !!FIELD_GET(ZL_OUTPUT_CTRL_STOP, out->ctrl);
+}
+
+/**
+ * zl3073x_out_stop - request a clean stop of an output
+ * @out: pointer to out state to update
+ *
+ * Sets the stop and stop_hz bits together, so the output goes high-Z
+ * rather than holding a fixed level once stopped. The stop is
+ * edge-aligned (the device waits for the proper edge before actually
+ * stopping).
+ */
+static inline void zl3073x_out_stop(struct zl3073x_out *out)
+{
+ FIELD_MODIFY(ZL_OUTPUT_CTRL_STOP, &out->ctrl, 1);
+ FIELD_MODIFY(ZL_OUTPUT_CTRL_STOP_HZ, &out->ctrl, 1);
+}
+
+/**
+ * zl3073x_out_start - request a clean restart of a stopped output
+ * @out: pointer to out state to update
+ *
+ * Clears the stop and stop_hz bits together. See zl3073x_out_stop().
+ */
+static inline void zl3073x_out_start(struct zl3073x_out *out)
+{
+ FIELD_MODIFY(ZL_OUTPUT_CTRL_STOP, &out->ctrl, 0);
+ FIELD_MODIFY(ZL_OUTPUT_CTRL_STOP_HZ, &out->ctrl, 0);
+}
+
+#define ZL3073X_OUT_PIN_F_CLOCK 0
+#define ZL3073X_OUT_PIN_F_GPO_CONST 1
+#define ZL3073X_OUT_PIN_F_GPO_STATUS 2
+#define ZL3073X_OUT_PIN_F_GPO_IRQ 3
+#define ZL3073X_OUT_PIN_F_GPO_UNKNOWN 4
+
+/**
+ * zl3073x_out_pin_func_get - get the function of an output pin
+ * @out: pointer to out state
+ * @id: output pin ID (even for P pin, odd for N pin)
+ *
+ * Report the current function of the given output pin. If GPO override is
+ * disabled the pin acts as a clock, otherwise it acts as a GPO with the
+ * mode selected by its GPO config control field.
+ *
+ * Return: one of the ZL3073X_OUT_PIN_F_* function codes
+ */
+static inline u8
+zl3073x_out_pin_func_get(const struct zl3073x_out *out, u8 id)
+{
+ u8 gpo_config;
+ bool gpo_en;
+
+ if (id & 1) {
+ gpo_en = FIELD_GET(ZL_OUTPUT_GPO_EN_OUT_N, out->gpo_en);
+ gpo_config = out->gpo_config_n;
+ } else {
+ gpo_en = FIELD_GET(ZL_OUTPUT_GPO_EN_OUT_P, out->gpo_en);
+ gpo_config = out->gpo_config_p;
+ }
+
+ if (!gpo_en)
+ return ZL3073X_OUT_PIN_F_CLOCK;
+
+ switch (FIELD_GET(ZL_OUTPUT_GPO_CONFIG_CTRL, gpo_config)) {
+ case ZL_OUTPUT_GPO_CONFIG_CTRL_OUTPUT:
+ return ZL3073X_OUT_PIN_F_GPO_CONST;
+ case ZL_OUTPUT_GPO_CONFIG_CTRL_STATUS:
+ return ZL3073X_OUT_PIN_F_GPO_STATUS;
+ case ZL_OUTPUT_GPO_CONFIG_CTRL_IRQ:
+ return ZL3073X_OUT_PIN_F_GPO_IRQ;
+ }
+
+ return ZL3073X_OUT_PIN_F_GPO_UNKNOWN;
+}
+
+/**
+ * zl3073x_out_pin_func_set - set the function of an output pin
+ * @out: pointer to out state to update
+ * @id: output pin ID (even for P pin, odd for N pin)
+ * @func: requested function, one of the ZL3073X_OUT_PIN_F_* codes
+ *
+ * Configure the given output pin as a clock or as a GPO in the requested
+ * mode by updating its GPO enable and GPO config control fields. Unknown
+ * function codes are ignored.
+ */
+static inline void
+zl3073x_out_pin_func_set(struct zl3073x_out *out, u8 id, u8 func)
+{
+ bool gpo_en = true;
+ u8 *gpo_config;
+ int ctrl = -1;
+
+ switch (func) {
+ case ZL3073X_OUT_PIN_F_CLOCK:
+ gpo_en = false;
+ break;
+ case ZL3073X_OUT_PIN_F_GPO_CONST:
+ ctrl = ZL_OUTPUT_GPO_CONFIG_CTRL_OUTPUT;
+ break;
+ case ZL3073X_OUT_PIN_F_GPO_STATUS:
+ ctrl = ZL_OUTPUT_GPO_CONFIG_CTRL_STATUS;
+ break;
+ case ZL3073X_OUT_PIN_F_GPO_IRQ:
+ ctrl = ZL_OUTPUT_GPO_CONFIG_CTRL_IRQ;
+ break;
+ default:
+ return;
+ }
+
+ if (id & 1) {
+ FIELD_MODIFY(ZL_OUTPUT_GPO_EN_OUT_N, &out->gpo_en, gpo_en);
+ gpo_config = &out->gpo_config_n;
+ } else {
+ FIELD_MODIFY(ZL_OUTPUT_GPO_EN_OUT_P, &out->gpo_en, gpo_en);
+ gpo_config = &out->gpo_config_p;
+ }
+
+ if (ctrl != -1)
+ FIELD_MODIFY(ZL_OUTPUT_GPO_CONFIG_CTRL, gpo_config, ctrl);
+}
+
/**
* zl3073x_out_is_ndiv - check if the given output is in N-div mode
* @out: pointer to out state
diff --git a/drivers/dpll/zl3073x/regs.h b/drivers/dpll/zl3073x/regs.h
index f3a5e1215aa36b..f17a2c78611827 100644
--- a/drivers/dpll/zl3073x/regs.h
+++ b/drivers/dpll/zl3073x/regs.h
@@ -94,6 +94,13 @@
#define ZL_REG_DIE_TEMP_STATUS ZL_REG(0, 0x44, 2)
+/*************************
+ * Register Page 1, GPIOs
+ *************************/
+
+#define ZL_REG_GPO_OUT(_idx) \
+ ZL_REG_IDX(_idx, 1, 0x70, 1, 3, 1)
+
/*************************
* Register Page 2, Status
*************************/
@@ -254,6 +261,8 @@
#define ZL_REG_OUTPUT_CTRL(_idx) \
ZL_REG_IDX(_idx, 9, 0x28, 1, ZL3073X_NUM_OUTS, 1)
#define ZL_OUTPUT_CTRL_EN BIT(0)
+#define ZL_OUTPUT_CTRL_STOP BIT(1)
+#define ZL_OUTPUT_CTRL_STOP_HZ BIT(3)
#define ZL_OUTPUT_CTRL_SYNTH_SEL GENMASK(6, 4)
#define ZL_REG_OUTPUT_STEP_TIME_MASK ZL_REG(9, 0x36, 2)
@@ -368,6 +377,17 @@
#define ZL_REG_OUTPUT_ESYNC_WIDTH ZL_REG(14, 0x18, 4)
#define ZL_REG_OUTPUT_PHASE_COMP ZL_REG(14, 0x20, 4)
+#define ZL_REG_OUTPUT_GPO_EN ZL_REG(14, 0x24, 1)
+#define ZL_OUTPUT_GPO_EN_OUT_P BIT(0)
+#define ZL_OUTPUT_GPO_EN_OUT_N BIT(1)
+
+#define ZL_REG_OUTPUT_GPO_CONFIG_OUT_P ZL_REG(14, 0x27, 1)
+#define ZL_REG_OUTPUT_GPO_CONFIG_OUT_N ZL_REG(14, 0x2a, 1)
+#define ZL_OUTPUT_GPO_CONFIG_CTRL GENMASK(2, 0)
+#define ZL_OUTPUT_GPO_CONFIG_CTRL_OUTPUT 1
+#define ZL_OUTPUT_GPO_CONFIG_CTRL_STATUS 3
+#define ZL_OUTPUT_GPO_CONFIG_CTRL_IRQ 4
+
/*
* Register Page 255 - HW registers access
*/
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [PATCH net-next 3/6] dpll: zl3073x: allow enabling/disabling output pins
2026-09-28 18:55 [PATCH net-next 0/6] dpll: zl3073x: add PTP periodic output support Ivan Vecera
2026-09-28 18:55 ` [PATCH net-next 1/6] dpll: zl3073x: notify sibling pin when shared output config changes Ivan Vecera
2026-09-28 18:55 ` [PATCH net-next 2/6] dpll: zl3073x: add GPO support for output pins Ivan Vecera
@ 2026-09-28 18:55 ` Ivan Vecera
2026-09-28 18:55 ` [PATCH net-next 4/6] dpll: zl3073x: consolidate pin capabilities into bitmap Ivan Vecera
` (2 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Ivan Vecera @ 2026-09-28 18:55 UTC (permalink / raw)
To: netdev
Cc: Chris du Quesnay, Arkadiusz Kubalewski, Jakub Kicinski,
Jiri Pirko, Min Li, Paolo Abeni, Petr Oros, Richard Cochran,
Vadim Fedorenko, linux-kernel
Add support for enabling and disabling individual output pins through
the DPLL subsystem's state_on_dpll_get/set callbacks, at the
granularity of a single P-pin or N-pin rather than the whole output.
zl3073x_dev_output_pin_state_get() reports the pin connection state and
zl3073x_dev_output_pin_state_set() applies the requested one.
Differential pins are toggled directly through the output_ctrl_x::stop
condition. CMOS pins are muted/unmuted via the GPO override, bracketed
by a clean stop/restart of the whole output since the GPO toggle is not
glitch-free. Output pins advertise DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE
to allow this from userspace.
Tested-by: Chris du Quesnay <Chris.duQuesnay@microchip.com>
Signed-off-by: Ivan Vecera <ivecera@redhat.com>
---
drivers/dpll/zl3073x/core.c | 78 +++++++++++++++++++++++++++++++++++++
drivers/dpll/zl3073x/core.h | 29 ++++++++++++++
drivers/dpll/zl3073x/dpll.c | 68 +++++++++++++++++++++++++++++++-
drivers/dpll/zl3073x/prop.c | 2 +
4 files changed, 175 insertions(+), 2 deletions(-)
diff --git a/drivers/dpll/zl3073x/core.c b/drivers/dpll/zl3073x/core.c
index 7386932df0327f..89cf46111dcb13 100644
--- a/drivers/dpll/zl3073x/core.c
+++ b/drivers/dpll/zl3073x/core.c
@@ -3,6 +3,7 @@
#include <linux/array_size.h>
#include <linux/bitfield.h>
#include <linux/bits.h>
+#include <linux/delay.h>
#include <linux/dev_printk.h>
#include <linux/device.h>
#include <linux/export.h>
@@ -12,6 +13,7 @@
#include <linux/regmap.h>
#include <linux/sprintf.h>
#include <linux/string_choices.h>
+#include <linux/time64.h>
#include <linux/unaligned.h>
#include <net/devlink.h>
@@ -667,6 +669,82 @@ int zl3073x_dev_gpo_set(struct zl3073x_dev *zldev, u8 gpo, bool value)
return zl3073x_write_u8(zldev, reg, val);
}
+/**
+ * zl3073x_dev_output_pin_state_set - enable or disable the given output pin
+ * @zldev: pointer to zl3073x_dev structure
+ * @id: output pin id
+ * @enable: true to enable the pin, false to disable it
+ *
+ * Differential output pins are enabled/disabled through the clean
+ * output_ctrl_x::stop condition, since they expose only a single logical
+ * pin.
+ *
+ * CMOS output pins are enabled/disabled by muting/unmuting the pin's
+ * driver via a GPO override. The GPO toggle is not glitch-free, so it is
+ * bracketed by a clean stop/restart of the whole output.
+ *
+ * Return: 0 on success, <0 on error
+ */
+int zl3073x_dev_output_pin_state_set(struct zl3073x_dev *zldev, u8 id,
+ bool enable)
+{
+ u8 out_id = zl3073x_output_pin_out_get(id);
+ struct zl3073x_out out;
+ u32 delay, freq;
+ int rc;
+
+ out = *zl3073x_out_state_get(zldev, out_id);
+
+ if (zl3073x_out_is_diff(&out)) {
+ if (enable)
+ zl3073x_out_start(&out);
+ else
+ zl3073x_out_stop(&out);
+
+ return zl3073x_out_state_set(zldev, out_id, &out);
+ }
+
+ /* Bracket the GPO override toggle below with a clean stop/restart,
+ * since the toggle itself is not glitch-free.
+ */
+ zl3073x_out_stop(&out);
+ rc = zl3073x_out_state_set(zldev, out_id, &out);
+ if (rc)
+ return rc;
+
+ /* output_ctrl_x::stop is edge-aligned, so the device can take up
+ * to half a period to actually reach the stopped state. Wait for
+ * that long plus 25 ms, to make sure it is really stopped before
+ * touching the GPO override below.
+ */
+ delay = 25 * USEC_PER_MSEC;
+ freq = zl3073x_dev_output_pin_freq_get(zldev, id);
+ if (freq)
+ delay += USEC_PER_SEC / 2 / freq;
+ fsleep(delay);
+
+ if (enable) {
+ zl3073x_out_pin_func_set(&out, id, ZL3073X_OUT_PIN_F_CLOCK);
+ } else {
+ rc = zl3073x_dev_gpo_set(zldev, id, false);
+ if (rc)
+ goto restart_output;
+ zl3073x_out_pin_func_set(&out, id, ZL3073X_OUT_PIN_F_GPO_CONST);
+ }
+
+ /* Restart the output regardless of the result below: on failure,
+ * don't leave the whole output, including the unrelated sibling
+ * pin, stopped indefinitely.
+ */
+ rc = zl3073x_out_state_set(zldev, out_id, &out);
+
+restart_output:
+ zl3073x_out_start(&out);
+ rc = zl3073x_out_state_set(zldev, out_id, &out) ? : rc;
+
+ return rc;
+}
+
/**
* zl3073x_ref_freq_meas_latch - latch reference frequency measurements
* @zldev: pointer to zl3073x_dev structure
diff --git a/drivers/dpll/zl3073x/core.h b/drivers/dpll/zl3073x/core.h
index e7064f2958fa5f..2b4785ba18b237 100644
--- a/drivers/dpll/zl3073x/core.h
+++ b/drivers/dpll/zl3073x/core.h
@@ -168,6 +168,8 @@ int zl3073x_write_hwreg_seq(struct zl3073x_dev *zldev,
int zl3073x_ref_phase_offsets_update(struct zl3073x_dev *zldev, int channel);
int zl3073x_dev_gpo_set(struct zl3073x_dev *zldev, u8 gpo, bool value);
+int zl3073x_dev_output_pin_state_set(struct zl3073x_dev *zldev, u8 id,
+ bool enable);
/**
* zl3073x_dev_is_ref_phase_comp_32bit - check ref phase comp register size
@@ -447,4 +449,31 @@ zl3073x_dev_output_pin_is_enabled(struct zl3073x_dev *zldev, u8 id)
return true;
}
+/**
+ * zl3073x_dev_output_pin_state_get - get the given output pin connection state
+ * @zldev: pointer to zl3073x device
+ * @id: output pin id
+ *
+ * Differential outputs are connected when their clock is not stopped.
+ * CMOS ones are connected when they are not GPO-overridden - the pin would
+ * not have been registered at all if its P/N side was not enabled by the
+ * signal format in the first place.
+ *
+ * Return: true if the output pin is connected, false if disconnected
+ */
+static inline bool
+zl3073x_dev_output_pin_state_get(struct zl3073x_dev *zldev, u8 id)
+{
+ u8 out_id = zl3073x_output_pin_out_get(id);
+ const struct zl3073x_out *out;
+
+ out = zl3073x_out_state_get(zldev, out_id);
+
+ if (zl3073x_out_is_stopped(out))
+ return false;
+
+ return zl3073x_out_is_diff(out) ||
+ zl3073x_out_pin_func_get(out, id) == ZL3073X_OUT_PIN_F_CLOCK;
+}
+
#endif /* _ZL3073X_CORE_H */
diff --git a/drivers/dpll/zl3073x/dpll.c b/drivers/dpll/zl3073x/dpll.c
index 2c6de4dab8b4ad..426974b0b5dc5c 100644
--- a/drivers/dpll/zl3073x/dpll.c
+++ b/drivers/dpll/zl3073x/dpll.c
@@ -17,6 +17,7 @@
#include <linux/ptp_clock_kernel.h>
#include <linux/slab.h>
#include <linux/sprintf.h>
+#include <linux/time64.h>
#include "core.h"
#include "dpll.h"
@@ -1195,12 +1196,74 @@ zl3073x_dpll_output_pin_state_on_dpll_get(const struct dpll_pin *dpll_pin,
enum dpll_pin_state *state,
struct netlink_ext_ack *extack)
{
- /* If the output pin is registered then it is always connected */
- *state = DPLL_PIN_STATE_CONNECTED;
+ struct zl3073x_dpll *zldpll = dpll_priv;
+ struct zl3073x_dev *zldev = zldpll->dev;
+ struct zl3073x_dpll_pin *pin = pin_priv;
+
+ guard(mutex)(&zldpll->lock);
+
+ if (zl3073x_dev_output_pin_state_get(zldev, pin->id))
+ *state = DPLL_PIN_STATE_CONNECTED;
+ else
+ *state = DPLL_PIN_STATE_DISCONNECTED;
return 0;
}
+/**
+ * zl3073x_dpll_output_pin_state_on_dpll_set - enable or disable an output pin
+ * @dpll_pin: registered dpll_pin
+ * @pin_priv: pointer to zl3073x_dpll_pin structure
+ * @dpll: registered dpll_device
+ * @dpll_priv: pointer to zl3073x_dpll structure
+ * @state: requested pin state
+ * @extack: netlink extack pointer
+ *
+ * Differential output pins are enabled/disabled through the clean
+ * output_ctrl_x::stop condition, since they expose only a single
+ * logical pin.
+ *
+ * CMOS output pins are enabled/disabled by muting/unmuting the pin's
+ * driver via a GPO override rather than by changing the output's
+ * signal_format, since a signal_format change is not glitch-free on
+ * this hardware. The GPO toggle itself is not glitch-free either, so
+ * it is bracketed by a clean stop/restart of the whole output.
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int
+zl3073x_dpll_output_pin_state_on_dpll_set(const struct dpll_pin *dpll_pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ enum dpll_pin_state state,
+ struct netlink_ext_ack *extack)
+{
+ struct zl3073x_dpll *zldpll = dpll_priv;
+ struct zl3073x_dev *zldev = zldpll->dev;
+ struct zl3073x_dpll_pin *pin = pin_priv;
+ bool enable;
+ int rc = 0;
+
+ if (state != DPLL_PIN_STATE_CONNECTED &&
+ state != DPLL_PIN_STATE_DISCONNECTED) {
+ NL_SET_ERR_MSG(extack, "Invalid pin state for output pin");
+ return -EINVAL;
+ }
+
+ guard(mutex)(&zldpll->lock);
+
+ enable = state == DPLL_PIN_STATE_CONNECTED;
+ if (zl3073x_dev_output_pin_state_get(zldev, pin->id) != enable) {
+ rc = zl3073x_dev_output_pin_state_set(zldev, pin->id, enable);
+ if (rc)
+ NL_SET_ERR_MSG(extack,
+ "Failed to change output pin state");
+ }
+
+ return rc;
+}
+
static int
zl3073x_dpll_nco_pin_operstate_on_dpll_get(const struct dpll_pin *dpll_pin,
void *pin_priv,
@@ -1685,6 +1748,7 @@ static const struct dpll_pin_ops zl3073x_dpll_output_pin_ops = {
.phase_adjust_get = zl3073x_dpll_output_pin_phase_adjust_get,
.phase_adjust_set = zl3073x_dpll_output_pin_phase_adjust_set,
.state_on_dpll_get = zl3073x_dpll_output_pin_state_on_dpll_get,
+ .state_on_dpll_set = zl3073x_dpll_output_pin_state_on_dpll_set,
};
static const struct dpll_pin_ops zl3073x_dpll_nco_pin_ops = {
diff --git a/drivers/dpll/zl3073x/prop.c b/drivers/dpll/zl3073x/prop.c
index ac9d41d0f978ef..806d1e6622da5a 100644
--- a/drivers/dpll/zl3073x/prop.c
+++ b/drivers/dpll/zl3073x/prop.c
@@ -214,6 +214,8 @@ struct zl3073x_pin_props *zl3073x_pin_props_get(struct zl3073x_dev *zldev,
u32 f;
props->dpll_props.type = DPLL_PIN_TYPE_GNSS;
+ props->dpll_props.capabilities =
+ DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
/* The output pin phase adjustment granularity equals half of
* the synth frequency count.
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [PATCH net-next 4/6] dpll: zl3073x: consolidate pin capabilities into bitmap
2026-09-28 18:55 [PATCH net-next 0/6] dpll: zl3073x: add PTP periodic output support Ivan Vecera
` (2 preceding siblings ...)
2026-09-28 18:55 ` [PATCH net-next 3/6] dpll: zl3073x: allow enabling/disabling " Ivan Vecera
@ 2026-09-28 18:55 ` Ivan Vecera
2026-09-28 18:55 ` [PATCH net-next 5/6] dpll: zl3073x: factor out output pin frequency helper Ivan Vecera
2026-09-28 18:55 ` [PATCH net-next 6/6] dpll: zl3073x: add PTP periodic output support Ivan Vecera
5 siblings, 0 replies; 7+ messages in thread
From: Ivan Vecera @ 2026-09-28 18:55 UTC (permalink / raw)
To: netdev
Cc: Chris du Quesnay, Arkadiusz Kubalewski, Jakub Kicinski,
Jiri Pirko, Min Li, Paolo Abeni, Petr Oros, Richard Cochran,
Vadim Fedorenko, linux-kernel
Replace the per-pin 'esync_control' boolean with a 'caps' bitmap in
struct zl3073x_dpll_pin. The bitmap is built from the pin properties
during pin registration and is easily extensible with further per-pin
capabilities.
No functional change intended.
Tested-by: Chris du Quesnay <Chris.duQuesnay@microchip.com>
Signed-off-by: Ivan Vecera <ivecera@redhat.com>
---
drivers/dpll/zl3073x/dpll.c | 27 +++++++++++++++++++++------
1 file changed, 21 insertions(+), 6 deletions(-)
diff --git a/drivers/dpll/zl3073x/dpll.c b/drivers/dpll/zl3073x/dpll.c
index 426974b0b5dc5c..fcf91aba2988af 100644
--- a/drivers/dpll/zl3073x/dpll.c
+++ b/drivers/dpll/zl3073x/dpll.c
@@ -38,7 +38,7 @@
* @dir: pin direction
* @id: pin id
* @prio: pin priority <0, 14>
- * @esync_control: embedded sync is controllable
+ * @caps: pin capabilities (ZL3073X_DPLL_PIN_CAP_*)
* @phase_gran: phase adjustment granularity
* @operstate: last saved operational state
* @phase_offset: last saved pin phase offset
@@ -55,7 +55,7 @@ struct zl3073x_dpll_pin {
enum dpll_pin_direction dir;
u8 id;
u8 prio;
- bool esync_control;
+ u8 caps;
s32 phase_gran;
enum dpll_pin_operstate operstate;
s64 phase_offset;
@@ -63,6 +63,17 @@ struct zl3073x_dpll_pin {
u32 measured_freq;
};
+/*
+ * DPLL pin capabilities
+ */
+enum zl3073x_dpll_pin_caps {
+ ZL3073X_DPLL_PIN_CAP_ESYNC_BIT,
+ ZL3073X_DPLL_PIN_CAPS_NBITS /* must be last */
+};
+
+#define __ZL3073X_DPLL_PIN_CAP(name) BIT(ZL3073X_DPLL_PIN_CAP_##name##_BIT)
+#define ZL3073X_DPLL_PIN_CAP_ESYNC __ZL3073X_DPLL_PIN_CAP(ESYNC)
+
/*
* Supported esync ranges for input and for output per output pair type
*/
@@ -189,7 +200,8 @@ zl3073x_dpll_input_pin_esync_get(const struct dpll_pin *dpll_pin,
ref_id = zl3073x_input_pin_ref_get(pin->id);
ref = zl3073x_ref_state_get(zldev, ref_id);
- if (!pin->esync_control || zl3073x_ref_freq_get(ref) <= 1)
+ if (!(pin->caps & ZL3073X_DPLL_PIN_CAP_ESYNC) ||
+ zl3073x_ref_freq_get(ref) <= 1)
return -EOPNOTSUPP;
esync->range = esync_freq_ranges;
@@ -897,7 +909,7 @@ zl3073x_dpll_output_pin_esync_get(const struct dpll_pin *dpll_pin,
synth_freq = zl3073x_synth_freq_get(synth);
out_freq = synth_freq / out->div;
- if (!pin->esync_control || out_freq <= 1)
+ if (!(pin->caps & ZL3073X_DPLL_PIN_CAP_ESYNC) || out_freq <= 1)
return -EOPNOTSUPP;
esync->range = esync_freq_ranges;
@@ -1837,14 +1849,17 @@ zl3073x_dpll_pin_register(struct zl3073x_dpll_pin *pin, u32 index)
if (IS_ERR(props))
return PTR_ERR(props);
- /* Save package label, fwnode, esync capability and phase adjust
+ /* Save package label, fwnode, capabilities and phase adjust
* granularity.
*/
strscpy(pin->label, props->package_label);
pin->fwnode = fwnode_handle_get(props->fwnode);
- pin->esync_control = props->esync_control;
pin->phase_gran = props->dpll_props.phase_gran;
+ pin->caps = 0;
+ if (props->esync_control)
+ pin->caps |= ZL3073X_DPLL_PIN_CAP_ESYNC;
+
if (zl3073x_dpll_is_input_pin(pin)) {
const struct zl3073x_chan *chan;
u8 ref;
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [PATCH net-next 5/6] dpll: zl3073x: factor out output pin frequency helper
2026-09-28 18:55 [PATCH net-next 0/6] dpll: zl3073x: add PTP periodic output support Ivan Vecera
` (3 preceding siblings ...)
2026-09-28 18:55 ` [PATCH net-next 4/6] dpll: zl3073x: consolidate pin capabilities into bitmap Ivan Vecera
@ 2026-09-28 18:55 ` Ivan Vecera
2026-09-28 18:55 ` [PATCH net-next 6/6] dpll: zl3073x: add PTP periodic output support Ivan Vecera
5 siblings, 0 replies; 7+ messages in thread
From: Ivan Vecera @ 2026-09-28 18:55 UTC (permalink / raw)
To: netdev
Cc: Chris du Quesnay, Arkadiusz Kubalewski, Jakub Kicinski,
Jiri Pirko, Min Li, Paolo Abeni, Petr Oros, Richard Cochran,
Vadim Fedorenko, linux-kernel
Extract the divisor and N-division computation from the output pin
frequency_set callback into zl3073x_dpll_output_pin_freq_set(), which
updates a struct zl3073x_out without committing it to hardware. This
lets the upcoming PTP periodic output support reuse the same P-pin,
N-pin and N-divided frequency handling.
No functional change intended.
Tested-by: Chris du Quesnay <Chris.duQuesnay@microchip.com>
Signed-off-by: Ivan Vecera <ivecera@redhat.com>
---
drivers/dpll/zl3073x/dpll.c | 135 ++++++++++++++++++++----------------
1 file changed, 74 insertions(+), 61 deletions(-)
diff --git a/drivers/dpll/zl3073x/dpll.c b/drivers/dpll/zl3073x/dpll.c
index fcf91aba2988af..0a36a2acf15b9f 100644
--- a/drivers/dpll/zl3073x/dpll.c
+++ b/drivers/dpll/zl3073x/dpll.c
@@ -1032,96 +1032,109 @@ zl3073x_dpll_output_pin_frequency_get(const struct dpll_pin *dpll_pin,
return 0;
}
+/**
+ * zl3073x_dpll_output_pin_freq_set - compute output config for pin frequency
+ * @pin: output pin to set the frequency for
+ * @out: output state to update, not committed to hardware
+ * @frequency: requested pin frequency in Hz
+ *
+ * Updates the divisor and N-division fields of @out so the given output
+ * pin runs at the requested frequency. For non N-divided formats the
+ * divisor is shared by both pins of the output pair. The caller is
+ * responsible for committing @out with zl3073x_out_state_set().
+ *
+ * Return: 0 on success, -EINVAL if the frequency cannot be represented
+ */
static int
-zl3073x_dpll_output_pin_frequency_set(const struct dpll_pin *dpll_pin,
- void *pin_priv,
- const struct dpll_device *dpll,
- void *dpll_priv, u64 frequency,
- struct netlink_ext_ack *extack)
+zl3073x_dpll_output_pin_freq_set(struct zl3073x_dpll_pin *pin,
+ struct zl3073x_out *out, u64 frequency)
{
- struct zl3073x_dpll *zldpll = dpll_priv;
- struct zl3073x_dev *zldev = zldpll->dev;
- struct zl3073x_dpll_pin *pin = pin_priv;
- struct zl3073x_dpll_pin *sibling = NULL;
- const struct zl3073x_synth *synth;
+ struct zl3073x_dev *zldev = pin->dpll->dev;
u32 new_div, synth_freq;
- struct zl3073x_out out;
- u8 out_id;
- int rc;
+ u8 synth;
- mutex_lock(&zldpll->lock);
-
- out_id = zl3073x_output_pin_out_get(pin->id);
- out = *zl3073x_out_state_get(zldev, out_id);
-
- /* Get attached synth frequency and compute new divisor */
- synth = zl3073x_synth_state_get(zldev, zl3073x_out_synth_get(&out));
- synth_freq = zl3073x_synth_freq_get(synth);
+ synth = zl3073x_out_synth_get(out);
+ synth_freq = zl3073x_dev_synth_freq_get(zldev, synth);
new_div = synth_freq / (u32)frequency;
- /* Check signal format */
- if (!zl3073x_out_is_ndiv(&out)) {
+ if (!zl3073x_out_is_ndiv(out)) {
/* For non N-divided signal formats the frequency is computed
* as division of synth frequency and output divisor, which
* is shared by both pins of the output pair.
*/
- out.div = new_div;
+ out->div = new_div;
/* For 50/50 duty cycle the divisor is equal to width */
- out.width = new_div;
-
- /* Commit output configuration */
- rc = zl3073x_out_state_set(zldev, out_id, &out);
- if (rc)
- goto unlock;
+ out->width = new_div;
- /* The other pin's frequency changed too - it has to be
- * notified about the change.
- */
- sibling = zl3073x_dpll_output_pin_sibling_get(pin);
-
- goto unlock;
+ return 0;
}
if (zl3073x_dpll_is_p_pin(pin)) {
- /* We are going to change output frequency for P-pin but
- * if the requested frequency is less than current N-pin
- * frequency then indicate a failure as we are not able
- * to compute N-pin divisor to keep its frequency unchanged.
- *
- * Update divisor for N-pin to keep N-pin frequency.
+ /* Changing the P-pin frequency, rescale the N-pin divisor to
+ * keep the N-pin frequency unchanged. Fail if the requested
+ * frequency is too low to represent the current N-pin one.
*/
- out.esync_n_period = (out.esync_n_period * out.div) / new_div;
- if (!out.esync_n_period) {
- rc = -EINVAL;
- goto unlock;
- }
+ out->esync_n_period = out->esync_n_period * out->div / new_div;
+ if (!out->esync_n_period)
+ return -EINVAL;
/* Update the output divisor */
- out.div = new_div;
+ out->div = new_div;
/* For 50/50 duty cycle the divisor is equal to width */
- out.width = out.div;
+ out->width = new_div;
} else {
- /* We are going to change frequency of N-pin but if
- * the requested freq is greater or equal than freq of P-pin
- * in the output pair we cannot compute divisor for the N-pin.
- * In this case indicate a failure.
- *
- * Update divisor for N-pin
+ /* Changing the N-pin frequency. Fail if the requested
+ * frequency is higher than or does not divide the P-pin one.
*/
- out.esync_n_period = div64_u64(synth_freq, frequency * out.div);
- if (!out.esync_n_period) {
- rc = -EINVAL;
- goto unlock;
- }
+ out->esync_n_period = div64_u64(synth_freq,
+ frequency * out->div);
+ if (!out->esync_n_period)
+ return -EINVAL;
}
/* For 50/50 duty cycle the divisor is equal to width */
- out.esync_n_width = out.esync_n_period;
+ out->esync_n_width = out->esync_n_period;
+
+ return 0;
+}
+
+static int
+zl3073x_dpll_output_pin_frequency_set(const struct dpll_pin *dpll_pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, u64 frequency,
+ struct netlink_ext_ack *extack)
+{
+ struct zl3073x_dpll *zldpll = dpll_priv;
+ struct zl3073x_dev *zldev = zldpll->dev;
+ struct zl3073x_dpll_pin *pin = pin_priv;
+ struct zl3073x_dpll_pin *sibling = NULL;
+ struct zl3073x_out out;
+ u8 out_id;
+ int rc;
+
+ mutex_lock(&zldpll->lock);
+
+ out_id = zl3073x_output_pin_out_get(pin->id);
+ out = *zl3073x_out_state_get(zldev, out_id);
+
+ rc = zl3073x_dpll_output_pin_freq_set(pin, &out, frequency);
+ if (rc)
+ goto unlock;
/* Commit output configuration */
rc = zl3073x_out_state_set(zldev, out_id, &out);
+ if (rc)
+ goto unlock;
+
+ /* For non N-divided formats the divisor is shared, so the other
+ * pin's frequency changed too and has to be notified.
+ */
+ if (!zl3073x_out_is_ndiv(&out))
+ sibling = zl3073x_dpll_output_pin_sibling_get(pin);
+
unlock:
mutex_unlock(&zldpll->lock);
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [PATCH net-next 6/6] dpll: zl3073x: add PTP periodic output support
2026-09-28 18:55 [PATCH net-next 0/6] dpll: zl3073x: add PTP periodic output support Ivan Vecera
` (4 preceding siblings ...)
2026-09-28 18:55 ` [PATCH net-next 5/6] dpll: zl3073x: factor out output pin frequency helper Ivan Vecera
@ 2026-09-28 18:55 ` Ivan Vecera
5 siblings, 0 replies; 7+ messages in thread
From: Ivan Vecera @ 2026-09-28 18:55 UTC (permalink / raw)
To: netdev
Cc: Chris du Quesnay, Arkadiusz Kubalewski, Jakub Kicinski,
Jiri Pirko, Min Li, Paolo Abeni, Petr Oros, Richard Cochran,
Vadim Fedorenko, linux-kernel
Register a PTP periodic output pin for each DPLL output pin that
supports step-time and declares 1 PPS (1 Hz) support in firmware.
The pins are named after the output pin (e.g. OUT5, OUT5P, OUT5N) and
any perout channel can be assigned to any of them.
Only 1 PPS is supported. Enabling a channel programs the pin assigned
to it for 1 Hz and connects it; disabling disconnects it, reusing the
output pin frequency helper and the per-pin connect/disconnect
primitive. A 1PPS bit is added to the per-pin capabilities.
Tested-by: Chris du Quesnay <Chris.duQuesnay@microchip.com>
Signed-off-by: Ivan Vecera <ivecera@redhat.com>
---
drivers/dpll/zl3073x/dpll.c | 211 ++++++++++++++++++++++++++++++++++++
drivers/dpll/zl3073x/dpll.h | 4 +
drivers/dpll/zl3073x/prop.h | 21 ++++
3 files changed, 236 insertions(+)
diff --git a/drivers/dpll/zl3073x/dpll.c b/drivers/dpll/zl3073x/dpll.c
index 0a36a2acf15b9f..f406d1e72530ff 100644
--- a/drivers/dpll/zl3073x/dpll.c
+++ b/drivers/dpll/zl3073x/dpll.c
@@ -68,11 +68,13 @@ struct zl3073x_dpll_pin {
*/
enum zl3073x_dpll_pin_caps {
ZL3073X_DPLL_PIN_CAP_ESYNC_BIT,
+ ZL3073X_DPLL_PIN_CAP_1PPS_BIT,
ZL3073X_DPLL_PIN_CAPS_NBITS /* must be last */
};
#define __ZL3073X_DPLL_PIN_CAP(name) BIT(ZL3073X_DPLL_PIN_CAP_##name##_BIT)
#define ZL3073X_DPLL_PIN_CAP_ESYNC __ZL3073X_DPLL_PIN_CAP(ESYNC)
+#define ZL3073X_DPLL_PIN_CAP_1PPS __ZL3073X_DPLL_PIN_CAP(1PPS)
/*
* Supported esync ranges for input and for output per output pair type
@@ -144,6 +146,19 @@ zl3073x_dpll_pin_get_by_ref(struct zl3073x_dpll *zldpll, u8 ref_id)
return NULL;
}
+static struct zl3073x_dpll_pin *
+zl3073x_dpll_output_pin_get_by_id(struct zl3073x_dpll *zldpll, u8 id)
+{
+ struct zl3073x_dpll_pin *pin;
+
+ list_for_each_entry(pin, &zldpll->pins, list) {
+ if (!zl3073x_dpll_is_input_pin(pin) && pin->id == id)
+ return pin;
+ }
+
+ return NULL;
+}
+
/**
* zl3073x_dpll_output_pin_sibling_get - get the other pin of an output pair
* @pin: output pin whose sibling is sought
@@ -1872,6 +1887,8 @@ zl3073x_dpll_pin_register(struct zl3073x_dpll_pin *pin, u32 index)
pin->caps = 0;
if (props->esync_control)
pin->caps |= ZL3073X_DPLL_PIN_CAP_ESYNC;
+ if (zl3073x_props_is_freq_supported(props, 1))
+ pin->caps |= ZL3073X_DPLL_PIN_CAP_1PPS;
if (zl3073x_dpll_is_input_pin(pin)) {
const struct zl3073x_chan *chan;
@@ -2823,6 +2840,159 @@ zl3073x_dpll_ptp_getmaxphase(struct ptp_clock_info *info __always_unused)
return NSEC_PER_SEC - 1;
}
+/**
+ * zl3073x_dpll_pin_is_perout_capable - check output pin perout eligibility
+ * @pin: output pin to check
+ *
+ * A registered output pin can be used for periodic output if its output
+ * supports step-time and the pin declares 1 PPS (1 Hz) support in firmware.
+ *
+ * Return: true if the pin can be used for periodic output.
+ */
+static bool
+zl3073x_dpll_pin_is_perout_capable(struct zl3073x_dpll_pin *pin)
+{
+ struct zl3073x_dev *zldev = pin->dpll->dev;
+ u8 out_id;
+
+ /* Periodic output is only available on output pins */
+ if (zl3073x_dpll_is_input_pin(pin) || zl3073x_dpll_is_nco_pin(pin))
+ return false;
+
+ out_id = zl3073x_output_pin_out_get(pin->id);
+
+ return zl3073x_dev_out_is_stepped(zldev, out_id) &&
+ (pin->caps & ZL3073X_DPLL_PIN_CAP_1PPS);
+}
+
+/**
+ * zl3073x_dpll_perout_enable - enable 1 PPS periodic output on a pin
+ * @pin: output pin to enable periodic output on
+ * @perout: periodic output request
+ *
+ * Programs the pin for 1 PPS (1 Hz) output and connects it.
+ *
+ * Context: Caller must hold pin->dpll->lock.
+ * Return: 0 on success, <0 on error
+ */
+static int
+zl3073x_dpll_perout_enable(struct zl3073x_dpll_pin *pin,
+ struct ptp_perout_request *perout)
+{
+ u8 out_id = zl3073x_output_pin_out_get(pin->id);
+ struct zl3073x_dev *zldev = pin->dpll->dev;
+ struct zl3073x_out out;
+ int rc;
+
+ /* Only 1 PPS (1 Hz) periodic output is supported */
+ if (perout->period.sec != 1 || perout->period.nsec)
+ return -EINVAL;
+
+ out = *zl3073x_out_state_get(zldev, out_id);
+
+ rc = zl3073x_dpll_output_pin_freq_set(pin, &out, 1);
+ if (rc)
+ return rc;
+
+ rc = zl3073x_out_state_set(zldev, out_id, &out);
+ if (rc)
+ return rc;
+
+ if (zl3073x_dev_output_pin_state_get(zldev, pin->id))
+ return 0;
+
+ return zl3073x_dev_output_pin_state_set(zldev, pin->id, true);
+}
+
+/**
+ * zl3073x_dpll_perout_disable - disable periodic output on a pin
+ * @pin: output pin to disable periodic output on
+ *
+ * Context: Caller must hold pin->dpll->lock.
+ * Return: 0 on success, <0 on error
+ */
+static int
+zl3073x_dpll_perout_disable(struct zl3073x_dpll_pin *pin)
+{
+ struct zl3073x_dev *zldev = pin->dpll->dev;
+
+ if (!zl3073x_dev_output_pin_state_get(zldev, pin->id))
+ return 0;
+
+ return zl3073x_dev_output_pin_state_set(zldev, pin->id, false);
+}
+
+static int zl3073x_dpll_ptp_verify(struct ptp_clock_info *info,
+ unsigned int pin_idx,
+ enum ptp_pin_function func,
+ unsigned int chan)
+{
+ /* Any perout pin can serve any perout channel, the channel range is
+ * validated by the PTP core. The requested pin is resolved from the
+ * channel via ptp_find_pin() in the enable callback.
+ */
+ switch (func) {
+ case PTP_PF_NONE:
+ case PTP_PF_PEROUT:
+ return 0;
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int zl3073x_dpll_ptp_enable(struct ptp_clock_info *info,
+ struct ptp_clock_request *rq, int on)
+{
+ struct zl3073x_dpll *zldpll = container_of(info, struct zl3073x_dpll,
+ ptp_info);
+ struct zl3073x_dpll_pin *pin = NULL;
+ struct zl3073x_dpll_pin *sibling;
+ int n, pin_idx, rc;
+ u8 id;
+
+ if (rq->type != PTP_CLK_REQ_PEROUT)
+ return -EOPNOTSUPP;
+
+ if (rq->perout.flags)
+ return -EOPNOTSUPP;
+
+ pin_idx = ptp_find_pin(zldpll->ptp_clock, PTP_PF_PEROUT,
+ rq->perout.index);
+ if (pin_idx < 0)
+ return -EINVAL;
+
+ n = pin_idx;
+ for_each_set_bit(id, zldpll->perout_map, ZL3073X_NUM_OUTPUT_PINS) {
+ if (!n) {
+ pin = zl3073x_dpll_output_pin_get_by_id(zldpll, id);
+ break;
+ }
+ n--;
+ }
+ if (!pin)
+ return -EINVAL;
+
+ mutex_lock(&zldpll->lock);
+ if (on)
+ rc = zl3073x_dpll_perout_enable(pin, &rq->perout);
+ else
+ rc = zl3073x_dpll_perout_disable(pin);
+ mutex_unlock(&zldpll->lock);
+
+ if (rc)
+ return rc;
+
+ /* Notify the affected output pin and, for shared-divisor formats,
+ * its sibling sharing the same HW output.
+ */
+ dpll_pin_change_ntf(pin->dpll_pin);
+ sibling = zl3073x_dpll_output_pin_sibling_get(pin);
+ if (sibling)
+ dpll_pin_change_ntf(sibling->dpll_pin);
+
+ return 0;
+}
+
static const struct ptp_clock_info zl3073x_dpll_ptp_clock_info = {
.owner = THIS_MODULE,
.max_adj = ZL3073X_DPLL_PTP_MAX_ADJ,
@@ -2832,6 +3002,8 @@ static const struct ptp_clock_info zl3073x_dpll_ptp_clock_info = {
.adjfine = zl3073x_dpll_ptp_adjfine,
.adjphase = zl3073x_dpll_ptp_adjphase,
.getmaxphase = zl3073x_dpll_ptp_getmaxphase,
+ .enable = zl3073x_dpll_ptp_enable,
+ .verify = zl3073x_dpll_ptp_verify,
};
/**
@@ -2843,16 +3015,53 @@ static const struct ptp_clock_info zl3073x_dpll_ptp_clock_info = {
static int zl3073x_dpll_ptp_register(struct zl3073x_dpll *zldpll)
{
struct zl3073x_dev *zldev = zldpll->dev;
+ struct ptp_pin_desc *pin_config;
+ struct zl3073x_dpll_pin *pin;
struct ptp_clock *ptp_clock;
+ unsigned int i;
+ u8 id;
zldpll->ptp_info = zl3073x_dpll_ptp_clock_info;
snprintf(zldpll->ptp_info.name, sizeof(zldpll->ptp_info.name),
"%s-dpll%u", dev_name(zldev->dev), zldpll->id);
+ /* Count output pins eligible for periodic output */
+ bitmap_zero(zldpll->perout_map, ZL3073X_NUM_OUTPUT_PINS);
+ list_for_each_entry(pin, &zldpll->pins, list)
+ if (zl3073x_dpll_pin_is_perout_capable(pin))
+ set_bit(pin->id, zldpll->perout_map);
+
+ zldpll->ptp_info.n_pins = bitmap_weight(zldpll->perout_map,
+ ZL3073X_NUM_OUTPUT_PINS);
+ zldpll->ptp_info.n_per_out = zldpll->ptp_info.n_pins;
+ if (!zldpll->ptp_info.n_pins)
+ goto no_pins;
+
+ pin_config = kzalloc_objs(*pin_config, zldpll->ptp_info.n_pins);
+ if (!pin_config)
+ return -ENOMEM;
+
+ i = 0;
+ for_each_set_bit(id, zldpll->perout_map, ZL3073X_NUM_OUTPUT_PINS) {
+ pin = zl3073x_dpll_output_pin_get_by_id(zldpll, id);
+ strscpy(pin_config[i].name, pin->label);
+ pin_config[i].index = i;
+ if (zl3073x_dev_output_pin_state_get(zldev, id)) {
+ pin_config[i].func = PTP_PF_PEROUT;
+ pin_config[i].chan = i;
+ }
+ i++;
+ }
+
+ zldpll->ptp_info.pin_config = pin_config;
+
+no_pins:
ptp_clock = ptp_clock_register(&zldpll->ptp_info, zldev->dev);
if (IS_ERR(ptp_clock)) {
dev_err(zldev->dev, "Failed to register PTP clock for DPLL%u\n",
zldpll->id);
+ kfree(zldpll->ptp_info.pin_config);
+ zldpll->ptp_info.pin_config = NULL;
return PTR_ERR(ptp_clock);
}
@@ -2871,6 +3080,8 @@ static void zl3073x_dpll_ptp_unregister(struct zl3073x_dpll *zldpll)
ptp_clock_unregister(zldpll->ptp_clock);
zldpll->ptp_clock = NULL;
}
+ kfree(zldpll->ptp_info.pin_config);
+ zldpll->ptp_info.pin_config = NULL;
}
/**
diff --git a/drivers/dpll/zl3073x/dpll.h b/drivers/dpll/zl3073x/dpll.h
index 993221dc63249d..b9ae6d8301031a 100644
--- a/drivers/dpll/zl3073x/dpll.h
+++ b/drivers/dpll/zl3073x/dpll.h
@@ -9,6 +9,8 @@
#include "core.h"
+struct zl3073x_dpll_pin;
+
/**
* struct zl3073x_dpll - ZL3073x DPLL sub-device structure
* @list: this DPLL list entry
@@ -25,6 +27,7 @@
* @pins: list of pins
* @ptp_info: PTP clock info
* @ptp_clock: registered PTP clock (or NULL)
+ * @perout_map: bitmap of output pins eligible for periodic output
*/
struct zl3073x_dpll {
struct list_head list;
@@ -41,6 +44,7 @@ struct zl3073x_dpll {
struct list_head pins;
struct ptp_clock_info ptp_info;
struct ptp_clock *ptp_clock;
+ DECLARE_BITMAP(perout_map, ZL3073X_NUM_OUTPUT_PINS);
};
struct zl3073x_dpll *zl3073x_dpll_alloc(struct zl3073x_dev *zldev, u8 ch);
diff --git a/drivers/dpll/zl3073x/prop.h b/drivers/dpll/zl3073x/prop.h
index 721a18f05938b8..c5f477b56d4ab1 100644
--- a/drivers/dpll/zl3073x/prop.h
+++ b/drivers/dpll/zl3073x/prop.h
@@ -31,4 +31,25 @@ struct zl3073x_pin_props *zl3073x_pin_props_get(struct zl3073x_dev *zldev,
void zl3073x_pin_props_put(struct zl3073x_pin_props *props);
+/**
+ * zl3073x_props_is_freq_supported - check if pin supports given frequency
+ * @props: pin properties
+ * @freq: frequency to check in Hz
+ *
+ * Return: true if the frequency is within the pin supported frequency ranges.
+ */
+static inline bool
+zl3073x_props_is_freq_supported(const struct zl3073x_pin_props *props, u64 freq)
+{
+ const struct dpll_pin_frequency *freqs;
+ int i;
+
+ freqs = props->dpll_props.freq_supported;
+ for (i = 0; i < props->dpll_props.freq_supported_num; i++)
+ if (freq >= freqs[i].min && freq <= freqs[i].max)
+ return true;
+
+ return false;
+}
+
#endif /* _ZL3073X_PROP_H */
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread