summaryrefslogtreecommitdiff
path: root/drivers/dpll/zl3073x/chan.c
blob: ba4d303d41b47d3b94925b5f03dbbbfcca1a047e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
// 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;
}