linux/drivers/dpll/zl3073x/chan.c
Ivan Vecera 2dbf9b7562 dpll: zl3073x: add channel ToD, phase step and TIE operations
Add low-level DPLL channel operations for ToD read/write/adjust,
output phase step, delta frequency offset write and TIE (Time
Interval Error) write. These serve as building blocks for the PTP
clock callbacks added in the next patch.

ToD operations use a wait-before-write pattern to avoid blocking
after each operation.

The tod_ready_wait helper selects the poll timeout based on the
current ToD command - write operations use a longer timeout (1000 ms)
than reads (30 ms).

The ToD read captures system timestamps (ptp_system_timestamp) around
the HW command and completion poll to support cross-timestamping.

The TIE write operation provides sub-picosecond resolution phase
adjustment for modes where the DPLL is tracking a reference
(AUTO and REFLOCK).

Add output step-time mask to struct zl3073x_dev and
zl3073x_dev_out_is_stepped() helper to check if an output
participates in step-time operations.

Reviewed-by: Petr Oros <poros@redhat.com>
Tested-by: Chris du Quesnay <Chris.duQuesnay@microchip.com>
Signed-off-by: Ivan Vecera <ivecera@redhat.com>
Link: https://patch.msgid.link/20260814082656.306534-3-ivecera@redhat.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
2026-08-18 09:40:05 -07:00

630 lines
17 KiB
C

// SPDX-License-Identifier: GPL-2.0-only
#include <linux/cleanup.h>
#include <linux/delay.h>
#include <linux/dev_printk.h>
#include <linux/ptp_clock_kernel.h>
#include <linux/string.h>
#include <linux/types.h>
#include "chan.h"
#include "core.h"
/**
* zl3073x_chan_state_update - update DPLL channel status from HW
* @zldev: pointer to zl3073x_dev structure
* @index: DPLL channel index
*
* Return: 0 on success, <0 on error
*/
int zl3073x_chan_state_update(struct zl3073x_dev *zldev, u8 index)
{
struct zl3073x_chan *chan = &zldev->chan[index];
u64 val;
int rc;
rc = zl3073x_read_u8(zldev, ZL_REG_DPLL_MON_STATUS(index),
&chan->mon_status);
if (rc)
return rc;
rc = zl3073x_read_u8(zldev, ZL_REG_DPLL_REFSEL_STATUS(index),
&chan->refsel_status);
if (rc)
return rc;
/* Read df_offset only when locked to a reference. In NCO mode
* df_offset was captured at entry by nco_mode_set() - preserve it.
*/
if (!zl3073x_chan_is_locked(chan)) {
if (!zl3073x_chan_mode_is_nco(chan))
chan->df_offset = ZL_DPLL_DF_OFFSET_UNKNOWN;
return 0;
}
rc = zl3073x_poll_zero_u8(zldev, ZL_REG_DPLL_DF_READ(index),
ZL_DPLL_DF_READ_SEM,
ZL_POLL_DF_READ_TIMEOUT_US);
if (rc)
return rc;
rc = zl3073x_write_u8(zldev, ZL_REG_DPLL_DF_READ(index),
ZL_DPLL_DF_READ_SEM | ZL_DPLL_DF_READ_REF_OFST);
if (rc)
return rc;
rc = zl3073x_poll_zero_u8(zldev, ZL_REG_DPLL_DF_READ(index),
ZL_DPLL_DF_READ_SEM,
ZL_POLL_DF_READ_TIMEOUT_US);
if (rc)
return rc;
rc = zl3073x_read_u48(zldev, ZL_REG_DPLL_DF_OFFSET(index), &val);
if (rc)
return rc;
chan->df_offset = sign_extend64(val, 47);
return 0;
}
/**
* zl3073x_chan_nco_mode_set - switch DPLL channel to NCO mode
* @zldev: pointer to zl3073x_dev structure
* @index: DPLL channel index
*
* Switches the channel to NCO mode and reads the df_offset
* auto-captured by nco_auto_read directly from the register.
* No DF_READ handshake is needed as nco_auto_read populates
* the register before the mode switch completes.
*
* Return: 0 on success, <0 on error
*/
int zl3073x_chan_nco_mode_set(struct zl3073x_dev *zldev, u8 index)
{
struct zl3073x_chan *chan = &zldev->chan[index];
u8 prev_mode, df_read;
u64 val;
int rc;
prev_mode = zl3073x_chan_mode_get(chan);
/* nco_auto_read captures the tracking offset at NCO entry only
* from reflock, auto or holdover mode. From freerun the captured
* value is not meaningful.
*/
if (prev_mode == ZL_DPLL_MODE_REFSEL_MODE_FREERUN) {
zl3073x_chan_mode_set(chan, ZL_DPLL_MODE_REFSEL_MODE_NCO);
rc = zl3073x_write_u8(zldev, ZL_REG_DPLL_MODE_REFSEL(index),
chan->mode_refsel);
if (rc) {
zl3073x_chan_mode_set(chan, prev_mode);
return rc;
}
chan->df_offset = ZL_DPLL_DF_OFFSET_UNKNOWN;
return 0;
}
/* Configure df_read for nco_auto_read:
* ref_ofst=0 - reads offset relative to master clock (not input ref)
* cmd=CMD_ACC_I - accumulated I-part covering both locked and
* holdover entry.
*
* No semaphore is set - this only configures what the df_offset
* value represents after the mode switch; nco_auto_read performs
* the actual read automatically.
*/
df_read = FIELD_PREP(ZL_DPLL_DF_READ_REF_OFST, 0) |
FIELD_PREP(ZL_DPLL_DF_READ_CMD, ZL_DPLL_DF_READ_CMD_ACC_I);
rc = zl3073x_write_u8(zldev, ZL_REG_DPLL_DF_READ(index), df_read);
if (rc)
return rc;
/* Wait for df_read configuration to take effect before
* triggering nco_auto_read via mode switch. The worst-case
* internal register update time is 25 ms.
*/
fsleep(25000);
zl3073x_chan_mode_set(chan, ZL_DPLL_MODE_REFSEL_MODE_NCO);
rc = zl3073x_write_u8(zldev, ZL_REG_DPLL_MODE_REFSEL(index),
chan->mode_refsel);
if (rc) {
zl3073x_chan_mode_set(chan, prev_mode);
return rc;
}
/* Wait for nco_auto_read to populate df_offset. The worst-case
* internal register update time is 25 ms.
*/
fsleep(25000);
/* Read df_offset captured by nco_auto_read during mode switch.
* No DF_READ semaphore handshake needed. Mode switch already
* succeeded, so don't propagate a read failure back to userspace.
*/
rc = zl3073x_read_u48(zldev, ZL_REG_DPLL_DF_OFFSET(index), &val);
if (rc) {
dev_warn(zldev->dev,
"Failed to read DPLL%u df_offset: %pe\n",
index, ERR_PTR(rc));
chan->df_offset = ZL_DPLL_DF_OFFSET_UNKNOWN;
} else {
chan->df_offset = sign_extend64(val, 47);
}
return 0;
}
/**
* zl3073x_chan_state_fetch - fetch DPLL channel state from hardware
* @zldev: pointer to zl3073x_dev structure
* @index: DPLL channel index to fetch state for
*
* Reads the mode_refsel, status and reference priority registers for
* the given DPLL channel and stores the values for later use.
*
* Return: 0 on success, <0 on error
*/
int zl3073x_chan_state_fetch(struct zl3073x_dev *zldev, u8 index)
{
struct zl3073x_chan *chan = &zldev->chan[index];
int rc, i;
rc = zl3073x_read_u8(zldev, ZL_REG_DPLL_CTRL(index), &chan->ctrl);
if (rc)
return rc;
rc = zl3073x_read_u8(zldev, ZL_REG_DPLL_MODE_REFSEL(index),
&chan->mode_refsel);
if (rc)
return rc;
dev_dbg(zldev->dev, "DPLL%u mode: %u, ref: %u\n", index,
zl3073x_chan_mode_get(chan), zl3073x_chan_ref_get(chan));
rc = zl3073x_chan_state_update(zldev, index);
if (rc)
return rc;
/* If firmware left the channel in NCO mode, mark df_offset as
* unknown - we cannot know whether the preconditions for a valid
* nco_auto_read capture were met.
*/
if (zl3073x_chan_mode_is_nco(chan))
chan->df_offset = ZL_DPLL_DF_OFFSET_UNKNOWN;
dev_dbg(zldev->dev,
"DPLL%u lock_state: %u, ho: %u, sel_state: %u, sel_ref: %u\n",
index, zl3073x_chan_lock_state_get(chan),
zl3073x_chan_is_ho_ready(chan) ? 1 : 0,
zl3073x_chan_refsel_state_get(chan),
zl3073x_chan_refsel_ref_get(chan));
guard(mutex)(&zldev->multiop_lock);
/* Read DPLL configuration from mailbox */
rc = zl3073x_mb_op(zldev, ZL_REG_DPLL_MB_SEM, ZL_DPLL_MB_SEM_RD,
ZL_REG_DPLL_MB_MASK, BIT(index));
if (rc)
return rc;
/* Read reference priority registers */
for (i = 0; i < ARRAY_SIZE(chan->ref_prio); i++) {
rc = zl3073x_read_u8(zldev, ZL_REG_DPLL_REF_PRIO(i),
&chan->ref_prio[i]);
if (rc)
return rc;
}
return 0;
}
/**
* zl3073x_chan_state_get - get current DPLL channel state
* @zldev: pointer to zl3073x_dev structure
* @index: DPLL channel index to get state for
*
* Return: pointer to given DPLL channel state
*/
const struct zl3073x_chan *zl3073x_chan_state_get(struct zl3073x_dev *zldev,
u8 index)
{
return &zldev->chan[index];
}
/**
* zl3073x_chan_tod_ready_wait - wait for ToD semaphore to clear
* @zldev: pointer to zl3073x device
* @ch: DPLL channel index
*
* Checks the ToD control register semaphore bit. If clear, returns
* immediately. Otherwise polls until the bit is cleared by the device.
*
* Return:
* * 0 - success
* * %-EBUSY - timeout
* * %-EOPNOTSUPP - unknown command detected
* * negative - other error
*/
int zl3073x_chan_tod_ready_wait(struct zl3073x_dev *zldev, u8 ch)
{
unsigned int timeout;
u8 tod_ctrl;
int rc;
rc = zl3073x_read_u8(zldev, ZL_REG_DPLL_TOD_CTRL(ch), &tod_ctrl);
if (rc)
return rc;
if (!(tod_ctrl & ZL_DPLL_TOD_CTRL_SEM))
return 0;
switch (FIELD_GET(ZL_DPLL_TOD_CTRL_CMD, tod_ctrl)) {
case ZL_DPLL_TOD_CTRL_CMD_WR_NEXT_1HZ:
timeout = ZL_POLL_TOD_WR_TIMEOUT_US;
break;
case ZL_DPLL_TOD_CTRL_CMD_RD_CURRENT:
case ZL_DPLL_TOD_CTRL_CMD_RD_NEXT_1HZ:
timeout = ZL_POLL_TOD_RD_TIMEOUT_US;
break;
default:
return -EOPNOTSUPP;
}
rc = zl3073x_poll_zero_u8(zldev, ZL_REG_DPLL_TOD_CTRL(ch),
ZL_DPLL_TOD_CTRL_SEM, timeout);
return rc == -ETIMEDOUT ? -EBUSY : rc;
}
/**
* zl3073x_chan_tod_ctrl - issue ToD command
* @zldev: pointer to zl3073x device
* @ch: DPLL channel index
* @cmd: ToD command to execute
*
* Writes the semaphore and command to dpll_tod_ctrl. The caller must
* ensure the device is ready (semaphore clear) before calling and
* must wait for completion if needed.
*
* Return: 0 on success, <0 on error
*/
static int zl3073x_chan_tod_ctrl(struct zl3073x_dev *zldev, u8 ch, u8 cmd)
{
return zl3073x_write_u8(zldev, ZL_REG_DPLL_TOD_CTRL(ch),
ZL_DPLL_TOD_CTRL_SEM | cmd);
}
/**
* zl3073x_chan_tod_read - read ToD registers after issuing a command
* @zldev: pointer to zl3073x device
* @ch: DPLL channel index
* @next_hz: if true, read predicted ToD at next 1 Hz; otherwise read current
* @ts: timespec to store the result
* @sts: optional system timestamp pair for cross-timestamping
*
* Context: Caller must serialize all zl3073x_chan_tod_* calls externally.
* Return: 0 on success, <0 on error
*/
int zl3073x_chan_tod_read(struct zl3073x_dev *zldev, u8 ch,
bool next_hz, struct timespec64 *ts,
struct ptp_system_timestamp *sts)
{
u32 nsec;
u64 sec;
u8 cmd;
int rc;
if (next_hz)
cmd = ZL_DPLL_TOD_CTRL_CMD_RD_NEXT_1HZ;
else
cmd = ZL_DPLL_TOD_CTRL_CMD_RD_CURRENT;
/* Wait for any previous ToD operation to complete */
rc = zl3073x_chan_tod_ready_wait(zldev, ch);
if (rc)
return rc;
ptp_read_system_prets(sts);
rc = zl3073x_chan_tod_ctrl(zldev, ch, cmd);
if (rc)
return rc;
rc = zl3073x_chan_tod_ready_wait(zldev, ch);
if (rc)
return rc;
ptp_read_system_postts(sts);
rc = zl3073x_read_u48(zldev, ZL_REG_DPLL_TOD_SEC(ch), &sec);
if (rc)
return rc;
/* HW nanoseconds are always in [0, NSEC_PER_SEC) range */
rc = zl3073x_read_u32(zldev, ZL_REG_DPLL_TOD_NS(ch), &nsec);
if (rc)
return rc;
ts->tv_sec = sec;
ts->tv_nsec = nsec;
return 0;
}
/**
* zl3073x_chan_tod_write - write ToD registers and trigger 1 Hz update
* @zldev: pointer to zl3073x device
* @ch: DPLL channel index
* @ts: time to set
*
* Context: Caller must serialize all zl3073x_chan_tod_* calls externally.
* Return: 0 on success, <0 on error
*/
int zl3073x_chan_tod_write(struct zl3073x_dev *zldev, u8 ch,
struct timespec64 ts)
{
int rc;
/* Wait for any previous ToD operation to complete */
rc = zl3073x_chan_tod_ready_wait(zldev, ch);
if (rc)
return rc;
rc = zl3073x_write_u48(zldev, ZL_REG_DPLL_TOD_SEC(ch), ts.tv_sec);
if (rc)
return rc;
rc = zl3073x_write_u32(zldev, ZL_REG_DPLL_TOD_NS(ch), ts.tv_nsec);
if (rc)
return rc;
return zl3073x_chan_tod_ctrl(zldev, ch,
ZL_DPLL_TOD_CTRL_CMD_WR_NEXT_1HZ);
}
/**
* zl3073x_chan_tod_adjust - atomic ToD read-modify-write with rollover guard
* @zldev: pointer to zl3073x device
* @ch: DPLL channel index
* @delta: time adjustment to apply
*
* Reads the next-Hz ToD and current ToD, then checks whether enough time
* remains before the next 1 Hz rollover to safely complete the write.
* Re-reads if the 1 Hz tick crossed between the two reads or if less
* than 20 ms remains before the next rollover. Applies @delta and writes
* the result back.
*
* Context: Caller must serialize all zl3073x_chan_tod_* calls externally.
* Return: 0 on success, <0 on error
*/
int zl3073x_chan_tod_adjust(struct zl3073x_dev *zldev, u8 ch,
struct timespec64 delta)
{
#define ZL_TOD_MAX_RETRIES 20
static const long threshold_ns = 20 * NSEC_PER_MSEC;
struct timespec64 ts_next, ts_cur, diff;
int rc, i;
for (i = 0; i < ZL_TOD_MAX_RETRIES; i++) {
rc = zl3073x_chan_tod_read(zldev, ch, true, &ts_next, NULL);
if (rc)
return rc;
rc = zl3073x_chan_tod_read(zldev, ch, false, &ts_cur, NULL);
if (rc)
return rc;
/* Ensure the 1 Hz tick did not cross between the two reads
* and that enough margin remains to complete the write.
*/
diff = timespec64_sub(ts_next, ts_cur);
if (diff.tv_sec > 0 ||
(!diff.tv_sec && diff.tv_nsec >= threshold_ns))
break;
}
if (i == ZL_TOD_MAX_RETRIES) {
dev_warn(zldev->dev,
"DPLL%u ToD adjust failed to get stable margin\n",
ch);
return -EBUSY;
}
/* Apply delta to the next-Hz ToD */
ts_next = timespec64_add(ts_next, delta);
if (!timespec64_valid_settod(&ts_next))
return -EINVAL;
return zl3073x_chan_tod_write(zldev, ch, ts_next);
#undef ZL_TOD_MAX_RETRIES
}
/**
* zl3073x_chan_df_offset_set - write delta frequency offset to hardware
* @zldev: pointer to zl3073x device
* @ch: DPLL channel index
* @offset: frequency offset in 2^-48 steps
*
* Context: Caller must hold the per-DPLL lock.
* Return: 0 on success, <0 on error
*/
int zl3073x_chan_df_offset_set(struct zl3073x_dev *zldev, u8 ch, s64 offset)
{
int rc;
rc = zl3073x_write_u48(zldev, ZL_REG_DPLL_DF_OFFSET(ch), offset);
if (!rc)
zldev->chan[ch].df_offset = offset;
return rc;
}
/**
* zl3073x_chan_tie_write - adjust DPLL phase using TIE write
* @zldev: pointer to zl3073x device
* @ch: DPLL channel index
* @delta_ns: phase adjustment in nanoseconds (must be in (-1s, 1s))
*
* Converts nanoseconds to TIE units (0.01 ps) and writes TIE data
* to the specified channel.
*
* Return: 0 on success, <0 on error
*/
int zl3073x_chan_tie_write(struct zl3073x_dev *zldev, u8 ch, s64 delta_ns)
{
s64 tie_data;
int rc;
guard(mutex)(&zldev->tie_lock);
/* Wait for any previous TIE operation to complete */
rc = zl3073x_poll_zero_u8(zldev, ZL_REG_DPLL_TIE_CTRL,
ZL_DPLL_TIE_CTRL_OP,
ZL_POLL_TIE_WR_TIMEOUT_US);
if (rc)
return rc;
/* Convert ns to TIE units (0.01 ps = 10^-14 s) */
tie_data = delta_ns * 100000LL;
rc = zl3073x_write_u48(zldev, ZL_REG_DPLL_TIE_DATA(ch), tie_data);
if (rc)
return rc;
rc = zl3073x_write_u8(zldev, ZL_REG_DPLL_TIE_CTRL_MASK, BIT(ch));
if (rc)
return rc;
return zl3073x_write_u8(zldev, ZL_REG_DPLL_TIE_CTRL,
ZL_DPLL_TIE_CTRL_OP_WR);
}
/**
* zl3073x_chan_phase_step - execute one output phase step operation
* @zldev: pointer to zl3073x device
* @ch: DPLL channel index
* @out_mask: bitmask of outputs to step
* @step_cycles: phase step in synthesizer clock cycles
* @tod_step: also step the ToD counter
*
* All masked outputs must use synthesizers of the same frequency since
* the step value is in synthesizer clock cycles.
*
* Return: 0 on success, <0 on error
*/
int zl3073x_chan_phase_step(struct zl3073x_dev *zldev, u8 ch,
u16 out_mask, s32 step_cycles,
bool tod_step)
{
u8 ctrl;
int rc;
guard(mutex)(&zldev->phase_step_lock);
/* Wait for any previous phase step operation to complete */
rc = zl3073x_poll_zero_u8(zldev, ZL_REG_OUTPUT_PHASE_STEP_CTRL,
ZL_OUTPUT_PHASE_STEP_CTRL_OP,
ZL_POLL_PHASE_STEP_TIMEOUT_US);
if (rc)
return rc;
rc = zl3073x_write_u32(zldev, ZL_REG_OUTPUT_PHASE_STEP_DATA,
step_cycles);
if (rc)
return rc;
rc = zl3073x_write_u16(zldev, ZL_REG_OUTPUT_PHASE_STEP_MASK, out_mask);
if (rc)
return rc;
rc = zl3073x_write_u8(zldev, ZL_REG_OUTPUT_PHASE_STEP_NUMBER, 1);
if (rc)
return rc;
ctrl = FIELD_PREP(ZL_OUTPUT_PHASE_STEP_CTRL_DPLL, ch) |
FIELD_PREP(ZL_OUTPUT_PHASE_STEP_CTRL_OP,
ZL_OUTPUT_PHASE_STEP_CTRL_OP_WRITE);
if (tod_step)
ctrl |= ZL_OUTPUT_PHASE_STEP_CTRL_TOD_STEP;
return zl3073x_write_u8(zldev, ZL_REG_OUTPUT_PHASE_STEP_CTRL, ctrl);
}
/**
* zl3073x_chan_state_set - commit DPLL channel state changes to hardware
* @zldev: pointer to zl3073x_dev structure
* @index: DPLL channel index to set state for
* @chan: desired channel state
*
* Skips the HW write if the configuration is unchanged, and otherwise
* writes only the changed registers to hardware. The mode_refsel register
* is written directly, while the reference priority registers are written
* via the DPLL mailbox interface.
*
* Return: 0 on success, <0 on HW error
*/
int zl3073x_chan_state_set(struct zl3073x_dev *zldev, u8 index,
const struct zl3073x_chan *chan)
{
struct zl3073x_chan *dchan = &zldev->chan[index];
int rc, i;
/* Skip HW write if configuration hasn't changed */
if (!memcmp(&dchan->cfg, &chan->cfg, sizeof(chan->cfg)))
return 0;
/* Direct register writes for ctrl and mode_refsel */
if (dchan->ctrl != chan->ctrl) {
rc = zl3073x_write_u8(zldev, ZL_REG_DPLL_CTRL(index),
chan->ctrl);
if (rc)
return rc;
dchan->ctrl = chan->ctrl;
}
if (dchan->mode_refsel != chan->mode_refsel) {
rc = zl3073x_write_u8(zldev, ZL_REG_DPLL_MODE_REFSEL(index),
chan->mode_refsel);
if (rc)
return rc;
dchan->mode_refsel = chan->mode_refsel;
}
/* Mailbox write for ref_prio if changed */
if (!memcmp(dchan->ref_prio, chan->ref_prio, sizeof(chan->ref_prio))) {
dchan->cfg = chan->cfg;
return 0;
}
guard(mutex)(&zldev->multiop_lock);
/* Read DPLL configuration into mailbox */
rc = zl3073x_mb_op(zldev, ZL_REG_DPLL_MB_SEM, ZL_DPLL_MB_SEM_RD,
ZL_REG_DPLL_MB_MASK, BIT(index));
if (rc)
return rc;
/* Update changed ref_prio registers */
for (i = 0; i < ARRAY_SIZE(chan->ref_prio); i++) {
if (dchan->ref_prio[i] != chan->ref_prio[i]) {
rc = zl3073x_write_u8(zldev,
ZL_REG_DPLL_REF_PRIO(i),
chan->ref_prio[i]);
if (rc)
return rc;
}
}
/* Commit DPLL configuration */
rc = zl3073x_mb_op(zldev, ZL_REG_DPLL_MB_SEM, ZL_DPLL_MB_SEM_WR,
ZL_REG_DPLL_MB_MASK, BIT(index));
if (rc)
return rc;
/* After successful write store new state */
dchan->cfg = chan->cfg;
return 0;
}