aboutsummaryrefslogtreecommitdiffstats
path: root/mpm/python/usrp_mpm/periph_manager/x4xx_clk_mgr.py
blob: bf845dd65998e5a91ac30432af7db9662815c00b (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
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
#
# Copyright 2021 Ettus Research, a National Instruments Company
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
"""
X400 Clocking Management

This module handles the analog clocks on the X4x0 motherboard. The clocking
architecture of the motherboard is spread out between a clocking auxiliary board,
which contains a GPS-displicined OCXO, but also connects an external reference
to the motherboard. It also houses a PLL for deriving a clock from the network
(eCPRI). The clocking aux board has its own control class (ClockingAuxBrdControl)
which also contains controls for the eCPRI PLL.

The motherboard itself has two main PLLs for clocking purposes: The Sample PLL
(also SPLL) is used to create all clocks used for RF-related purposes. It creates
the sample clock (a very fast clock, ~3 GHz) and the PLL reference clock (PRC)
which is used as a timebase for the daughterboard CPLD and a reference for the
LO synthesizers (50-64 MHz).

Its input is the base reference clock (BRC). This clock comes either from the
clocking aux board, which in turn can provide a reference from the OCXO (with or
without GPS-disciplining) or from the external reference input SMA port.
The BRC is typically 10-25 MHz.

The BRC can also come from the reference PLL (RPLL), when the clock source is
set to 'mboard'. The RPLL produces clocks that are consumed by the GTY banks
(for Ethernet and Aurora), but it can also generate a reference clock for
the SPLL. By default, its reference is a fixed 100 MHz clock, but it can also be
driven by the eCPRI PLL, which itself can be driven by a clock from the GTY banks,
which is the case if the clock source is set to 'nsync'.

The master clock rate (MCR) is not actually controlled in this module, but it
depends on the sample clock rate. It also depends on the RFDC settings, so it is
controlled in x4xx.py, which has access to both RFDC and X4xxClockMgr.

Block diagram (for more details, see the schematic)::

 ┌────────────────────────────────────────────────────────┐
 │ Clocking Aux Board                                     │
 │  ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐           │
 │  │ GPSDO  ├─> OCXO   │ │External│ │eCPRI/  │           │
 │  │        │ │        │ │        │ │nsync   │           │
 │  └────────┘ └────┬───┘ └───┬────┘ └────────┘           │
 │                  │         │                           │
 │                  │         │                           │
 │      ┌───────────v─────────v───┐   ┌───────────┐       │
 │      │                         │   │eCPRI PLL  │       │
 │      └┐          MUX          ┌┘   │LMK05318   │       │
 │       └─┐                   ┌─┘    │           │       │
 │         └─┬─────────────────┘      └──┬────────┘       │
 │           │                           │                │
 └───────────┼───────────────────────────┼────────────────┘
             │                           │
             │  ┌─────────────┐          │
          ┌──v──v┐            │          │
          │ MUX  │            │          │  ┌───── 100 MHz
          └──┬───┘            │          │  │
             │Base Ref. Clock │          │  │
     ┌───────v───────┐        │  ┌───────v──v──┐
     │ Sample PLL    │        └──┤Reference PLL│
     │ LMK04832      │           │LMK03328     │
     │               │           │             │
     │               │           │             │
     └──┬─────────┬──┘           └────┬────────┘
        │         │                   │
        │         │                   │
        v         v                   v
    Sample      PLL Reference        GTY Banks
    Clock       Clock


The code in this module controls the RPLL and SPLL as well as some of the muxing.
The eCPRI PLL is controlled from the ClockingAuxBrdControl class.
Most importantly, this class controls the sequencing of configuring clocks. This
means it won't only switch muxes and configure PLLs, but will also do things in
the right order, and put components in reset where necessary.
For this reason, it requires callbacks to reset RFDC and daughterboard clocks.
"""

from enum import Enum
from usrp_mpm import lib # Pulls in everything from C++-land
from usrp_mpm.sys_utils import i2c_dev
from usrp_mpm.sys_utils.gpio import Gpio
from usrp_mpm.sys_utils.udev import dt_symbol_get_spidev
from usrp_mpm.periph_manager.x4xx_periphs import MboardRegsControl
from usrp_mpm.periph_manager.x4xx_sample_pll import LMK04832X4xx
from usrp_mpm.periph_manager.x4xx_reference_pll import LMK03328X4xx
from usrp_mpm.periph_manager.x4xx_clk_aux import ClockingAuxBrdControl
from usrp_mpm.mpmutils import poll_with_timeout
from usrp_mpm.rpc_server import no_rpc

# this is not the frequency out of the GPSDO(GPS Lite, 20MHz) itself but
# the GPSDO on the CLKAUX board is used to fine tune the OCXO via EFC
# which is running at 10MHz
X400_GPSDO_OCXO_CLOCK_FREQ = 10e6
X400_RPLL_I2C_LABEL = 'rpll_i2c'
X400_DEFAULT_RPLL_REF_SOURCE = '100M_reliable_clk'
X400_DEFAULT_MGT_CLOCK_RATE = 156.25e6
X400_DEFAULT_INT_CLOCK_FREQ = 25e6

class X4xxClockMgr:
    """
    X4x0 Clocking Manager

    The clocking subsystem of X4x0 is very complex. This class is designed to
    capture all clocking-related logic specific to the X4x0.

    This class controls clock and time sources.
    """
    CLOCK_SOURCE_MBOARD = "mboard"
    CLOCK_SOURCE_INTERNAL = ClockingAuxBrdControl.SOURCE_INTERNAL
    CLOCK_SOURCE_EXTERNAL = ClockingAuxBrdControl.SOURCE_EXTERNAL
    CLOCK_SOURCE_GPSDO = ClockingAuxBrdControl.SOURCE_GPSDO
    CLOCK_SOURCE_NSYNC = ClockingAuxBrdControl.SOURCE_NSYNC

    TIME_SOURCE_INTERNAL = "internal"
    TIME_SOURCE_EXTERNAL = "external"
    TIME_SOURCE_GPSDO = "gpsdo"
    TIME_SOURCE_QSFP0 = "qsfp0"

    # All valid sync_sources for X4xx in the form of (clock_source, time_source)
    valid_sync_sources = {
        (CLOCK_SOURCE_MBOARD, TIME_SOURCE_INTERNAL),
        (CLOCK_SOURCE_INTERNAL, TIME_SOURCE_INTERNAL),
        (CLOCK_SOURCE_EXTERNAL, TIME_SOURCE_EXTERNAL),
        (CLOCK_SOURCE_EXTERNAL, TIME_SOURCE_INTERNAL),
        (CLOCK_SOURCE_GPSDO, TIME_SOURCE_GPSDO),
        (CLOCK_SOURCE_GPSDO, TIME_SOURCE_INTERNAL),
        (CLOCK_SOURCE_NSYNC, TIME_SOURCE_INTERNAL),
    }

    class SetSyncRetVal(Enum):
        OK = 'OK'
        NOP = 'nop'
        FAIL = 'failure'

    def __init__(self,
                 clock_source,
                 time_source,
                 ref_clock_freq,
                 sample_clock_freq,
                 is_legacy_mode,
                 clk_aux_board,
                 cpld_control,
                 log):
        # Store parent objects
        self.log = log.getChild("ClkMgr")
        self._cpld_control = cpld_control
        self._clocking_auxbrd = clk_aux_board
        self._time_source = time_source
        self._clock_source = clock_source
        self._int_clock_freq = X400_DEFAULT_INT_CLOCK_FREQ
        self._ext_clock_freq = ref_clock_freq
        # Preallocate other objects to satisfy linter
        self.mboard_regs_control = None
        self._sample_pll = None
        self._reference_pll = None
        self._rpll_i2c_bus = None
        self._base_ref_clk_select = None
        self._set_reset_rfdc = lambda **kwargs: None
        self._set_reset_db_clocks = lambda *args: None
        self._rpll_reference_sources = {}
        # Init peripherals
        self._init_available_srcs()
        self._init_clk_peripherals()
        # Now initialize the clocks themselves
        self._init_ref_clock_and_time(
            clock_source,
            ref_clock_freq,
            sample_clock_freq,
            is_legacy_mode,
        )
        self._init_meas_clock()
        self._cpld_control.enable_pll_ref_clk()

    ###########################################################################
    # Initialization code
    ###########################################################################
    def _init_available_srcs(self):
        """
        Initialize the available clock and time sources.
        """
        has_gps = self._clocking_auxbrd and self._clocking_auxbrd.is_gps_supported()
        self._avail_clk_sources = [self.CLOCK_SOURCE_MBOARD]
        if self._clocking_auxbrd:
            self._avail_clk_sources.extend([
                self.CLOCK_SOURCE_INTERNAL,
                self.CLOCK_SOURCE_EXTERNAL])
            if self._clocking_auxbrd.is_nsync_supported():
                self._avail_clk_sources.append(self.CLOCK_SOURCE_NSYNC)
            if has_gps:
                self._avail_clk_sources.append(self.CLOCK_SOURCE_GPSDO)
        self.log.trace(f"Available clock sources are: {self._avail_clk_sources}")
        self._avail_time_sources = [
            self.TIME_SOURCE_INTERNAL, self.TIME_SOURCE_EXTERNAL, self.TIME_SOURCE_QSFP0]
        if has_gps:
            self._avail_time_sources.append(self.TIME_SOURCE_GPSDO)
        self.log.trace("Available time sources are: {}".format(self._avail_time_sources))

    def _init_clk_peripherals(self):
        """
        Initialize objects for peripherals owned by this class. Most importantly,
        this includes the RPLL and SPLL control classes.
        """
        # Create SPI and I2C interfaces to the LMK registers
        spll_spi_node = dt_symbol_get_spidev('spll')
        sample_lmk_regs_iface = lib.spi.make_spidev_regs_iface(
            spll_spi_node,
            1000000,    # Speed (Hz)
            0x3,        # SPI mode
            8,          # Addr shift
            0,          # Data shift
            1<<23,      # Read flag
            0,          # Write flag
        )
        # Initialize I2C connection to RPLL
        self._rpll_i2c_bus = i2c_dev.dt_symbol_get_i2c_bus(X400_RPLL_I2C_LABEL)
        if self._rpll_i2c_bus is None:
            raise RuntimeError("RPLL I2C bus could not be found")
        reference_lmk_regs_iface = lib.i2c.make_i2cdev_regs_iface(
            self._rpll_i2c_bus,
            0x54,   # addr
            False,  # ten_bit_addr
            100,    # timeout_ms
            1       # reg_addr_size
        )
        self._sample_pll = LMK04832X4xx(sample_lmk_regs_iface, self.log)
        self._reference_pll = LMK03328X4xx(reference_lmk_regs_iface, self.log)
        # Init BRC select GPIO control
        self._base_ref_clk_select = Gpio('BASE-REFERENCE-CLOCK-SELECT', Gpio.OUTPUT, 1)

    def _init_ref_clock_and_time(self,
                                 clock_source,
                                 ref_clock_freq,
                                 sample_clock_freq,
                                 is_legacy_mode,
                                 ):
        """
        Initialize clock and time sources. After this function returns, the
        reference signals going to the FPGA are valid.

        This is only called once, during __init__(). Calling it again will set
        clocks to defaults, but is also redundant since clocks do not need to be
        initialized twice.
        """
        # A dictionary of tuples (source #, rate) corresponding to each
        # available RPLL reference source.
        # source # 1 => PRIREF source
        # source # 2 => SECREF source
        self._rpll_reference_sources = {X400_DEFAULT_RPLL_REF_SOURCE: (2, 100e6)}
        reference_rates = [None, None]
        for source, rate in self._rpll_reference_sources.values():
            reference_rates[source-1] = rate
        self._reference_pll.reference_rates = reference_rates
        # Now initializes and reconfigure all clocks.
        # If clock_source and ref_clock_freq are not provided, they will not be changed.
        # If any other parameters are not provided, they will be configured with
        # default values.
        self._reset_clocks(value=True, reset_list=['cpld'])
        if clock_source is not None:
            self._set_brc_source(clock_source)
        if ref_clock_freq is not None:
            self._set_ref_clock_freq(ref_clock_freq)
        self._config_rpll(
            X400_DEFAULT_MGT_CLOCK_RATE,
            X400_DEFAULT_INT_CLOCK_FREQ,
            X400_DEFAULT_RPLL_REF_SOURCE)
        self._config_spll(sample_clock_freq, is_legacy_mode)
        self._reset_clocks(value=False, reset_list=['cpld'])

    def _init_meas_clock(self):
        """
        Initialize the TDC measurement clock. After this function returns, the
        FPGA TDC meas_clock is valid.
        """
        # This may or may not be used for X400. Leave as a place holder
        self.log.debug("TDC measurement clock not yet implemented.")

    ###########################################################################
    # Public APIs (that are not exposed as MPM calls)
    ###########################################################################
    @no_rpc
    def set_rfdc_reset_cb(self, rfdc_reset_cb):
        """
        Set reference to RFDC control. Ideally, we'd get that in __init__(), but
        due to order of operations, it's not ready yet when we call that.
        """
        self._set_reset_rfdc = rfdc_reset_cb

    @no_rpc
    def set_dboard_reset_cb(self, db_reset_cb):
        """
        Set reference to RFDC control. Ideally, we'd get that in __init__(), but
        due to order of operations, it's not ready yet when we call that.
        """
        self._set_reset_db_clocks = db_reset_cb

    @no_rpc
    def unset_cbs(self):
        """
        Removes any stored references to our owning X4xx class instance
        """
        self._set_reset_rfdc = None
        self._set_reset_db_clocks = None

    @no_rpc
    def config_pps_to_timekeeper(self, master_clock_rate):
        """ Configures the path from the PPS to the timekeeper"""
        pps_source = "internal_pps" \
            if self._time_source == self.TIME_SOURCE_INTERNAL \
            else "external_pps"
        self._sync_spll_clocks(pps_source)
        self._configure_pps_forwarding(True, master_clock_rate)

    @no_rpc
    def get_clock_sources(self):
        """
        Lists all available clock sources.
        """
        return self._avail_clk_sources

    @no_rpc
    def get_time_sources(self):
        " Returns list of valid time sources "
        return self._avail_time_sources

    @no_rpc
    def get_ref_clock_freq(self):
        " Returns the currently active reference clock frequency (BRC) "
        clock_source = self.get_clock_source()
        if clock_source == self.CLOCK_SOURCE_MBOARD:
            return self._int_clock_freq
        if clock_source == self.CLOCK_SOURCE_GPSDO:
            return X400_GPSDO_OCXO_CLOCK_FREQ
        # clock_source == "external":
        return self._ext_clock_freq

    @no_rpc
    def get_ref_locked(self):
        """
        Return lock status both RPLL and SPLL.
        """
        ref_pll_status = self._reference_pll.get_status()
        sample_pll_status = self._sample_pll.get_status()
        return all([
            ref_pll_status['PLL1 lock'],
            ref_pll_status['PLL2 lock'],
            sample_pll_status['PLL1 lock'],
            sample_pll_status['PLL2 lock'],
        ])

    @no_rpc
    def set_spll_rate(self, sample_clock_freq, is_legacy_mode):
        """
        Safely set the output rate of the sample PLL.

        This will do the required resets.
        """
        self._reset_clocks(value=True, reset_list=('rfdc', 'cpld', 'db_clock'))
        self._config_spll(sample_clock_freq, is_legacy_mode)
        self._reset_clocks(value=False, reset_list=('rfdc', 'cpld', 'db_clock'))

    @no_rpc
    def set_sync_source(self, clock_source, time_source):
        """
        Selects reference clock and PPS sources. Unconditionally re-applies the
        time source to ensure continuity between the reference clock and time
        rates.
        Note that if we change the source such that the time source is changed
        to 'external', then we need to also disable exporting the reference
        clock (RefOut and PPS-In are the same SMA connector).
        """
        assert (clock_source, time_source) in self.valid_sync_sources, \
            f'Clock and time source pair ({clock_source}, {time_source}) is ' \
            'not a valid selection'
        # Now see if we can keep the current settings, or if we need to run an
        # update of sync sources:
        if (clock_source == self._clock_source) and (time_source == self._time_source):
            spll_status = self._sample_pll.get_status()
            rpll_status = self._reference_pll.get_status()
            if (spll_status['PLL1 lock'] and spll_status['PLL2 lock'] and
                    rpll_status['PLL1 lock'] and rpll_status['PLL2 lock']):
                # Nothing change no need to do anything
                self.log.trace("New sync source assignment matches "
                               "previous assignment. Ignoring update command.")
                return self.SetSyncRetVal.NOP
            self.log.debug(
                "Although the clock source has not changed, some PLLs "
                "are not locked. Setting clock source again...")
            self.log.trace("- SPLL status: {}".format(spll_status))
            self.log.trace("- RPLL status: {}".format(rpll_status))
        # Start setting sync source
        self.log.debug(
            f"Setting sync source to time_source={time_source}, "
            f"clock_source={clock_source}")
        self._time_source = time_source
        # Reset downstream clocks (excluding RPLL)
        self._reset_clocks(value=True, reset_list=('db_clock', 'cpld', 'rfdc', 'spll'))
        self._set_brc_source(clock_source)
        return self.SetSyncRetVal.OK

    @no_rpc
    def set_clock_source_out(self, enable=True):
        """
        Allows routing the clock configured as source on the clk aux board to
        the RefOut terminal. This only applies to internal, gpsdo and nsync.
        """
        clock_source = self.get_clock_source()
        if self.get_time_source() == self.TIME_SOURCE_EXTERNAL:
            raise RuntimeError(
                'Cannot export clock when using external time reference!')
        if clock_source not in self._clocking_auxbrd.VALID_CLK_EXPORTS:
            raise RuntimeError(f"Invalid source to export: `{clock_source}'")
        if self._clocking_auxbrd is None:
            raise RuntimeError("No clocking aux board available")
        return self._clocking_auxbrd.export_clock(enable)

    ###########################################################################
    # Top-level BIST APIs
    #
    # These calls will be available as MPM calls. They are only needed by BIST.
    ###########################################################################
    def enable_ecpri_clocks(self, enable=True, clock='both'):
        """
        Enable or disable the export of FABRIC and GTY_RCV eCPRI
        clocks. Main use case until we support eCPRI is manufacturing
        testing.
        """
        self.mboard_regs_control.enable_ecpri_clocks(enable, clock)

    def nsync_change_input_source(self, source):
        """
        Switches the input reference source of the clkaux lmk (the "eCPRI PLL").

        Valid options are: fabric_clk, gty_rcv_clk, and sec_ref.

        fabric_clk and gty_rcv_clk are clock sources from the mboard.
        They are both inputs to the primary reference source of the clkaux lmk.
        sec_ref is the default reference select for the clkaux lmk, it has
        two inputs: Ref in or internal and GPS mode

        Only a public API for the BIST.
        """
        assert source in (
            self._clocking_auxbrd.SOURCE_NSYNC_LMK_PRI_FABRIC_CLK,
            self._clocking_auxbrd.SOURCE_NSYNC_LMK_PRI_GTY_RCV_CLK,
            self._clocking_auxbrd.NSYNC_SEC_REF,
        )
        if source == self._clocking_auxbrd.SOURCE_NSYNC_LMK_PRI_FABRIC_CLK:
            self.enable_ecpri_clocks(True, 'fabric')
            self._clocking_auxbrd.set_nsync_ref_select(
                self._clocking_auxbrd.NSYNC_PRI_REF)
            self._clocking_auxbrd.set_nsync_pri_ref_source(source)
        elif source == self._clocking_auxbrd.SOURCE_NSYNC_LMK_PRI_GTY_RCV_CLK:
            self.enable_ecpri_clocks(True, 'gty_rcv')
            self._clocking_auxbrd.set_nsync_ref_select(
                self._clocking_auxbrd.NSYNC_PRI_REF)
            self._clocking_auxbrd.set_nsync_pri_ref_source(source)
        else:
            self._clocking_auxbrd.set_nsync_ref_select(
                self._clocking_auxbrd.NSYNC_SEC_REF)

    def config_rpll_to_nsync(self):
        """
        Configures the rpll to use the LMK28PRIRefClk output
        by the clkaux LMK
        """
        # LMK28PRIRefClk only available when nsync is source, as lmk
        # is powered off otherwise
        self.set_sync_source(clock_source='nsync', time_source=self._time_source)

        # Add LMK28PRIRefClk as an available RPLL reference source
        # 1 => PRIREF source; source is output at 25 MHz
        # TODO: enable out4 on LMK
        previous_ref_rate = self._reference_pll.reference_rates[0]
        self._rpll_reference_sources['clkaux_nsync_clk'] = (1, 25e6)
        self._reference_pll.reference_rates[0] = 25e6
        self._config_rpll(X400_DEFAULT_MGT_CLOCK_RATE,
                          X400_DEFAULT_INT_CLOCK_FREQ,
                          'clkaux_nsync_clk')

        # remove clkaux_nsync_clk as a valid reference source for later calls
        # to _config_rpll(), it is only valid in this configuration
        self._reference_pll.reference_rates[0] = previous_ref_rate
        del self._rpll_reference_sources['clkaux_nsync_clk']

    def get_fpga_aux_ref_freq(self):
        """
        Return the tick count of an FPGA counter which measures the width of
        the PPS signal on the FPGA_AUX_REF FPGA input using a 40 MHz clock.
        Main use case until we support eCPRI is manufacturing testing.
        A return value of 0 indicates absence of a valid PPS signal on the
        FPGA_AUX_REF line.
        """
        return self.mboard_regs_control.get_fpga_aux_ref_freq()

    ###########################################################################
    # Top-level APIs
    #
    # These calls will be available as MPM calls
    ###########################################################################
    def get_clock_source(self):
        " Return the currently selected clock source "
        return self._clock_source

    def get_time_source(self):
        " Return the currently selected time source "
        return self._time_source

    def get_spll_freq(self):
        """ Returns the output frequency setting of the SPLL """
        return self._sample_pll.output_freq

    def get_prc_rate(self):
        """
        Returns the rate of the PLL Reference Clock (PRC) which is
        routed to the daughterboards.
        Note: The ref clock will change if the sample clock frequency
        is modified.
        """
        prc_clock_map = {
            2.94912e9:  61.44e6,
            3e9:        62.5e6,
            # 3e9:      50e6, RF Legacy mode will be checked separately
            3.072e9:    64e6,
        }

        # RF Legacy Mode always has a PRC rate of 50 MHz
        if self._sample_pll.is_legacy_mode:
            return 50e6
        # else:
        return prc_clock_map[self.get_spll_freq()]

    def set_ref_clk_tuning_word(self, tuning_word, out_select=0):
        """
        Set the tuning word for the clocking aux board DAC. This wull update the
        tuning word used by the DAC.
        """
        if self._clocking_auxbrd is not None:
            self._clocking_auxbrd.config_dac(tuning_word, out_select)
        else:
            raise RuntimeError("No clocking aux board available")

    def get_ref_clk_tuning_word(self, out_select=0):
        """
        Get the tuning word configured for the clocking aux board DAC.
        """
        if self._clocking_auxbrd is None:
            raise RuntimeError("No clocking aux board available")
        return self._clocking_auxbrd.read_dac(out_select)

    def store_ref_clk_tuning_word(self, tuning_word):
        """
        Store the given tuning word in the clocking aux board ID EEPROM.
        """
        if self._clocking_auxbrd is not None:
            self._clocking_auxbrd.store_tuning_word(tuning_word)
        else:
            raise RuntimeError("No clocking aux board available")

    def get_sync_sources(self):
        """
        Enumerates permissible sync sources.
        """
        return [{
            "time_source": time_source,
            "clock_source": clock_source
        } for (clock_source, time_source) in self.valid_sync_sources]


    ###########################################################################
    # Low-level controls
    ###########################################################################
    def _reset_clocks(self, value, reset_list):
        """
        Shuts down all clocks downstream to upstream or clears reset on all
        clocks upstream to downstream. Specify the list of clocks to reset in
        reset_list. The order of clocks specified in the reset_list does not
        affect the order in which the clocks are reset.
        """
        if value:
            self.log.trace("Reset clocks: {}".format(reset_list))
            if 'db_clock' in reset_list:
                self._set_reset_db_clocks(value)
            if 'cpld' in reset_list:
                self._cpld_control.enable_pll_ref_clk(enable=False)
            if 'rfdc' in reset_list:
                self._set_reset_rfdc(reset=True)
            if 'spll' in reset_list:
                self._sample_pll.reset(value, hard=True)
            if 'rpll' in reset_list:
                self._reference_pll.reset(value, hard=True)
        else:
            self.log.trace("Bring clocks out of reset: {}".format(reset_list))
            if 'rpll' in reset_list:
                self._reference_pll.reset(value, hard=True)
            if 'spll' in reset_list:
                self._sample_pll.reset(value, hard=True)
            if 'rfdc' in reset_list:
                self._set_reset_rfdc(reset=False)
            if 'cpld' in reset_list:
                self._cpld_control.enable_pll_ref_clk(enable=True)
            if 'db_clock' in reset_list:
                self._set_reset_db_clocks(value)

    def _config_rpll(self, usr_clk_rate, internal_brc_rate, internal_brc_source):
        """
        Configures the LMK03328 to generate the desired MGT reference clocks
        and internal BRC rate.

        Currently, the MGT protocol selection is not supported, but a custom
        usr_clk_rate can be generated from PLL1.

        usr_clk_rate - the custom clock rate to generate from PLL1
        internal_brc_rate - the rate to output as the BRC
        internal_brc_source - the reference source which drives the RPLL
        """
        if internal_brc_source not in self._rpll_reference_sources:
            self.log.error('Invalid internal BRC source of {} was selected.'
                           .format(internal_brc_source))
            raise RuntimeError('Invalid internal BRC source of {} was selected.'
                               .format(internal_brc_source))
        ref_select = self._rpll_reference_sources[internal_brc_source][0]

        # If the desired rate matches the rate of the primary reference source,
        # directly passthrough that reference source
        if internal_brc_rate == self._reference_pll.reference_rates[0]:
            brc_select = 'bypass'
        else:
            brc_select = 'PLL'
        self._reference_pll.init()
        self._reference_pll.config(
            ref_select, internal_brc_rate, usr_clk_rate, brc_select)
        # The internal BRC rate will only change when _config_rpll is called
        # with a new internal BRC rate
        self._int_clock_freq = internal_brc_rate

    def _config_spll(self, sample_clock_freq, is_legacy_mode):
        """
        Configures the SPLL for the specified master clock rate.
        """
        self._sample_pll.init()
        self._sample_pll.config(sample_clock_freq, self.get_ref_clock_freq(),
                                is_legacy_mode)

    def _set_brc_source(self, clock_source):
        """
        Switches the Base Reference Clock Source between internal, external,
        mboard, and gpsdo using the GPIO pin and clocking aux board control.
        internal is a clock source internal to the clocking aux board, but
        external to the motherboard.
        Should not be called outside of set_sync_source or _init_ref_clock_and_time
        without proper reset and reconfig of downstream clocks.
        """
        if clock_source == self.CLOCK_SOURCE_MBOARD:
            self._base_ref_clk_select.set(1)
            if self._clocking_auxbrd:
                self._clocking_auxbrd.export_clock(False)
        else:
            if self._clocking_auxbrd is None:
                self.log.error('Invalid BRC selection {}. No clocking aux '
                               'board was found.'.format(clock_source))
                raise RuntimeError('Invalid BRC selection {}'.format(clock_source))
            self._base_ref_clk_select.set(0)
            if clock_source == self.CLOCK_SOURCE_EXTERNAL:
                # This case is a bit special: We also need to tell the clocking
                # aux board if we plan to consume the external time reference or
                # not.
                time_src_board = \
                    ClockingAuxBrdControl.SOURCE_EXTERNAL \
                    if self._time_source == self.TIME_SOURCE_EXTERNAL \
                    else ClockingAuxBrdControl.SOURCE_INTERNAL
                self._clocking_auxbrd.set_source(
                    ClockingAuxBrdControl.SOURCE_EXTERNAL, time_src_board)
            elif clock_source == self.CLOCK_SOURCE_INTERNAL:
                self._clocking_auxbrd.set_source(ClockingAuxBrdControl.SOURCE_INTERNAL)
            elif clock_source == self.CLOCK_SOURCE_GPSDO:
                self._clocking_auxbrd.set_source(ClockingAuxBrdControl.SOURCE_GPSDO)
            elif clock_source == self.CLOCK_SOURCE_NSYNC:
                self._clocking_auxbrd.set_source(ClockingAuxBrdControl.SOURCE_NSYNC)
            else:
                self.log.error('Invalid BRC selection {}'.format(clock_source))
                raise RuntimeError('Invalid BRC selection {}'.format(clock_source))
        self._clock_source = clock_source
        self.log.debug(f"Base reference clock source is: {clock_source}")

    def _sync_spll_clocks(self, pps_source="internal_pps"):
        """
        Synchronize base reference clock (BRC) and PLL reference clock (PRC)
        at start of PPS trigger.

        Uses the LMK 04832 pll1_r_divider_sync to synchronize BRC with PRC.
        This sync method uses a callback to actual trigger the sync. Before
        the trigger is pulled (CLOCK_CTRL_PLL_SYNC_TRIGGER) PPS source is
        configured base on current reference clock and pps_source. After sync
        trigger the method waits for 1sec for the CLOCK_CTRL_PLL_SYNC_DONE
        to go high.

        :param pps_source: select whether internal ("internal_pps") or external
                           ("external_pps") PPS should be used. This parameter
                           is taken into account when the current clock source
                           is external. If the current clock source is set to
                           internal then this parameter is not taken into
                           account.
        :return:           success state of sync call
        :raises RuntimeError: for invalid combinations of reference clock and
                              pps_source
        """
        def select_pps():
            """
            Select PPS source based on current clock source and pps_source.

            This returns the bits for the motherboard CLOCK_CTRL register that
            control the PPS source.
            """
            EXT_PPS = "external_pps"
            INT_PPS = "internal_pps"
            PPS_SOURCES = (EXT_PPS, INT_PPS)
            assert pps_source in PPS_SOURCES, \
                "%s not in %s" % (pps_source, PPS_SOURCES)

            supported_configs = {
                (10E6, EXT_PPS): MboardRegsControl.CLOCK_CTRL_PPS_EXT,
                (10E6, INT_PPS): MboardRegsControl.CLOCK_CTRL_PPS_INT_10MHz,
                (25E6, INT_PPS): MboardRegsControl.CLOCK_CTRL_PPS_INT_25MHz
            }

            config = (self.get_ref_clock_freq(), pps_source)
            if config not in supported_configs:
                raise RuntimeError("Unsupported combination of reference clock "
                                   "(%.2E) and PPS source (%s) for PPS sync." %
                                   config)
            return supported_configs[config]

        return self._sample_pll.pll1_r_divider_sync(
            lambda: self.mboard_regs_control.pll_sync_trigger(select_pps()))

    def _configure_pps_forwarding(self, enable, master_clock_rate, delay=1.0):
        """
        Configures the PPS forwarding to the sample clock domain (master
        clock rate). This function assumes _sync_spll_clocks function has
        already been executed.

        :param enable: Boolean to choose whether PPS is forwarded to the
                       sample clock domain.

        :param delay:  Delay in seconds from the PPS rising edge to the edge
                       occurence in the application. This value has to be in
                       range 0 < x <= 1. In order to forward the PPS signal
                       from base reference clock to sample clock an aligned
                       rising edge of the clock is required. This can be
                       created by the _sync_spll_clocks function. Based on the
                       greatest common divisor of the two clock rates there
                       are multiple occurences of an aligned edge each second.
                       One of these aligned edges has to be chosen for the
                       PPS forwarding by setting this parameter.

        :return:       None, Exception on error
        """
        return self.mboard_regs_control.configure_pps_forwarding(
            enable, master_clock_rate, self.get_prc_rate(), delay)

    def _set_ref_clock_freq(self, freq):
        """
        Tell our USRP what the frequency of the external reference clock is.

        Will throw if it's not a valid value.
        """
        if (freq < 1e6) or (freq > 50e6):
            raise RuntimeError('External reference clock frequency is out of the valid range.')
        if (freq % 40e3) != 0:
            # TODO: implement exception of a 50e3 step size for 200MSPS
            raise RuntimeError('External reference clock frequency is of incorrect step size.')
        self._ext_clock_freq = freq
        # If the external source is currently selected we also need to re-apply the
        # time_source. This call also updates the dboards' rates.
        if self.get_clock_source() == self.CLOCK_SOURCE_EXTERNAL:
            self.set_sync_source(self._clock_source, self._time_source)