File: x4xx_clock_policy.py

package info (click to toggle)
uhd 4.9.0.0%2Bds1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 184,180 kB
  • sloc: cpp: 262,887; python: 112,011; ansic: 102,670; vhdl: 57,031; tcl: 19,924; xml: 8,581; makefile: 3,028; sh: 2,812; pascal: 230; javascript: 120; csh: 94; asm: 20; perl: 11
file content (879 lines) | stat: -rw-r--r-- 37,094 bytes parent folder | download
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
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
#
# Copyright 2022 Ettus Research, a National Instruments Company
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
"""X4xx Clocking Policies.

These clocking policies are sets of rules for configuring the various clocks on
X4xx motherboards.
"""
import math
from collections import OrderedDict
from dataclasses import dataclass, field

from usrp_mpm.chips import LMK04832
from usrp_mpm.dboard_manager import FBX, ZBX
from usrp_mpm.mpmutils import parse_multi_device_arg
from usrp_mpm.periph_manager.x4xx_clock_lookup import MCR_LMK_VCO, RFDC_PLL_CONFIGS
from usrp_mpm.periph_manager.x4xx_clock_types import Spll1Vco
from usrp_mpm.periph_manager.x4xx_rfdc_ctrl import X4xxRfdcCtrl
from usrp_mpm.periph_manager.x4xx_sample_pll import LMK04832X4xx


###############################################################################
# Define Dataclasses for component settings
###############################################################################
# pylint: disable=too-many-instance-attributes
@dataclass
class RfdcConfig:
    """Provide all relevant RFDC settings.

    Since we can have different settings per daughterboard, we have one of these
    per daughterboard.
    """

    # The frequency that the RFDC PLL will output
    conv_rate: float
    # This is both the interpolation and decimation factor. Because we always
    # have one radio block per daughterboard, the radio rates for Tx and Rx are
    # always identical, and thus the resampling factor is the same, too.
    resampling: int = 1


@dataclass
class SpllConfig:
    """Provide all relevant SPLL settings."""

    # The reference frequency for the SPLL (e.g. from the external reference input,
    # often 10 MHz)
    ref_freq: float
    # The frequency that is generated at the SPLL output for the ADC/DACs. In
    # other words, the reference frequency for the RFDC PLLs.
    output_freq: float
    # Output divider for the ADC/DAC clock signal
    output_divider: int
    # The output divider for the PRC output (PRC is thus PLL2 VCO rate divided
    # by this)
    prc_divider: int
    vcxo_freq: Spll1Vco
    sysref_div: int
    clkin0_r_div: int
    pll1_n_div: int
    pll2_prescaler: int
    pll2_n_cal_div: int
    pll2_n_div: int
    # PRC output to the daughterboard
    prc_to_db: bool


@dataclass
class X4xxClockConfig:
    """Stores all clock-related settings to achieve a requested master clock rate.

    Consolidates settings for the main PLL (SPLL, the LMK04832) as well as for
    the RFDC and MMCM.
    """

    spll_config: SpllConfig
    rfdc_configs: list  # list of RfdcConfig
    # If bitfile defaults should be used set to true
    mmcm_use_defaults: bool
    # The feedback divider of the MMCM
    mmcm_feedback_divider: int = 1
    # The output divider of the MMCM (dict of clock name and div value)
    mmcm_output_div_map: dict = field(default_factory=dict)
    # Input divider of the MMCM
    mmcm_input_divider: int = 1


def lcm(x, y):
    """Least common multiple.

    This can be taken from math if we upgrade to Python >= 3.9.0
    """
    x = int(x)
    y = int(y)
    return int(x * y / math.gcd(x, y))


# pylint: enable=too-many-instance-attributes


class X4xxClockPolicy:
    """Base class for X4xx clock policies.

    Such a policy would be used by the X4xxClockManager to determine settings.
    """

    def __init__(self, args, log):
        """Initialize the X4xx clock policy."""
        self._initial_args = args
        self.args = args

    def set_dsp_info(self, dsp_info):
        """Store the DSP info of the current FPGA image.

        The individual clock policies can choose what to do with this info.
        """
        raise NotImplementedError()

    def get_default_mcr(self):
        """Return a reasonable default master clock rate.

        This method is called during initialization, to decide which rate to
        configure the device to, when no MCR is given by the user.

        This method is called twice: Once before set_dsp_info() is called, and
        once afterwards. The first time a valid, sensible clock rate must be
        returned which can be used to initialize the device enough to enable
        the FPGA. When the FPGA is running, the DSP info can be read back and
        then this function is queried again, because the first time around, we
        may have chosen an MCR value that is not useful for the capabilities of
        this particular bitfile. It is therefore valid to return different values
        depending on whether or not the DSP info is set.
        """
        raise NotImplementedError()

    def get_radio_clock_rate(self, mcr):
        """Return the radio clock rates for the current configuration.

        The individual clock policies can choose how to calculate this info.
        """
        raise NotImplementedError()

    def get_num_rates(self):
        """Returns the number of different master clock rates we can handle.

        If it is a value greater than one, then we need to be able to
        handle vectors of MCRs in the following functions.
        """
        raise NotImplementedError()

    def validate_ref_clock_freq(self, ref_clock_freq, master_clock_rates):
        """Verify that ref_clock_freq is a valid reference clock frequency.

        Checks whether ref_clock_freq is valid for the given master clock rates.

        Will throw a RuntimeError if not.

        Does not modify the state of this policy.
        """

    def get_intermediate_clk_settings(self, ref_clk_freq, old_mcrs, new_mcrs):
        """Returns an intermediate clock settings object.

        An  intermediate object is necessary if going from the old to
        the new master clock rates would fail otherwise.
        """
        raise NotImplementedError()

    def coerce_mcr(self, master_clock_rates):
        """Validate that the requested master clock rate is valid.

        May coerce the master clock rates if not, but may also throw a ValueError
        if there is no reasonable way to coerce the desired rates.

        Must not modify the state of this policy.
        """
        raise NotImplementedError()

    def get_config(self, ref_clock_freq, master_clock_rates):
        """Generate the clock configuration (This is where the action happens).

        This will return a X4xxClockConfig class which can be used by X4xxClockMgr
        to actually configure various clocks.

        This method may change the state of this class.
        """
        raise NotImplementedError()

    def should_reboot_on_reconfiguration(self):
        """This returns if MPM should reboot on a clocking reconfiguration."""
        raise NotImplementedError()


class X410ClockPolicy(X4xxClockPolicy):
    """This is the clocking policy for X440.

    This is a pretty simple, everything is hardcoded.
    Some properties:

    - We only allow 3 different converter rates
    - RFdc PLL is unused
    - Only one rate is allowed (same rate on both dboards)
    """

    DEFAULT_MASTER_CLOCK_RATE = 122.88e6  # Keep this a low value

    master_to_sample_clk = OrderedDict(
        {
            #      MCR:    (DSP BW, SPLL, decim, legacy mode)
            122.88e6 * 4: (400, 2.94912e9, 2, False),  # RF (1M-8G)
            122.88e6 * 2: (200, 2.94912e9, 2, False),  # RF (1M-8G)
            122.88e6 * 1: (100, 2.94912e9, 8, False),  # RF (1M-8G)
            125e6 * 1: (100, 3.00000e9, 8, False),  # RF (1M-8G)
            125e6 * 2: (200, 3.00000e9, 2, False),  # RF (1M-8G)
            125e6 * 4: (400, 3.00000e9, 2, False),  # RF (1M-8G)
            200e6: (400, 3.00000e9, 4, True),  # RF (Legacy Mode)
        }
    )

    def __init__(self, mboard_info, dboard_infos, args, log):
        """Initialize the X410 clock policy."""
        super().__init__(args, log)
        self._dsp_info = None
        self._dsp_bw = None

    def set_dsp_info(self, dsp_info):
        """Store the DSP info of the current FPGA image."""
        self._dsp_info = dsp_info
        if self._dsp_info[0]["num_rx_chans"] == 0 and self._dsp_info[0]["num_tx_chans"] == 0:
            return

        # this part of DSP initialization is only relevant for radio frontends
        self._dsp_bw = dsp_info[0]["bw"]
        assert self._dsp_bw in [x[0] for x in self.master_to_sample_clk.values()]

    def get_default_mcr(self):
        """Return a reasonable default master clock rate."""
        if self._dsp_bw is None:
            return self.DEFAULT_MASTER_CLOCK_RATE
        for mcr, cfg in self.master_to_sample_clk.items():
            if cfg[0] == self._dsp_bw:
                return mcr
        raise AssertionError("Cannot determine default MCR.")

    def get_radio_clock_rate(self, mcr):
        """Return the radio clock rates for the current configuration."""
        return mcr / self._dsp_info[0]["spc_rx"]

    # pylint: disable=no-self-use
    def get_num_rates(self):
        """Returns the number of different master clock rates we can handle."""
        return 1

    # pylint: enable=no-self-use

    def validate_ref_clock_freq(self, ref_clock_freq, master_clock_rates):
        """Verify that ref_clock_freq is a valid reference clock frequency.

        Checks whether ref_clock_freq is valid for the given master clock rates.

        Will throw a RuntimeError if not.

        Does not modify the state of this policy.
        """
        self.coerce_mcr(master_clock_rates)
        mcr = master_clock_rates[0]
        is_legacy_mode = self.master_to_sample_clk[mcr][3]
        step_size = 50e3 if is_legacy_mode else 40e3
        if ref_clock_freq % step_size != 0:
            raise RuntimeError("External reference clock frequency is of incorrect step size.")

    def get_intermediate_clk_settings(self, ref_clk_freq, old_mcrs, new_mcrs):
        """Returns an intermediate clock settings object.

        An  intermediate object is necessary if going from the old to
        the new master clock rates would fail otherwise.
        """
        return None

    def coerce_mcr(self, master_clock_rates):
        """Validate that the requested master clock rate is valid.

        Will throw a ValueError if not.

        Does not modify the state of this policy.
        """
        assert len(master_clock_rates) == 1
        mcr = master_clock_rates[0]
        if mcr not in self.master_to_sample_clk or (
            self._dsp_bw and self.master_to_sample_clk[mcr][0] != self._dsp_bw
        ):
            raise ValueError(
                f"Invalid master clock rate: {mcr/1e6} MHz for current FPGA image "
                f"with bandwidth {self._dsp_bw} MHz!"
            )
        return master_clock_rates

    def get_config(self, ref_clock_freq, master_clock_rates):
        """This is where the action happens: Generate the clock configuration."""
        assert ref_clock_freq
        assert master_clock_rates
        self.coerce_mcr(master_clock_rates)
        mcr = master_clock_rates[0]
        rfdc_freq = self.master_to_sample_clk[mcr][1]
        legacy_mode = self.master_to_sample_clk[mcr][3]
        pfd1 = {2.94912e9: 40e3, 3e9: 50e3, 3.072e9: 40e3}[rfdc_freq]
        spll_args = {
            2.94912e9: {
                "ref_freq": ref_clock_freq,
                "output_freq": rfdc_freq,
                "output_divider": 1,
                "prc_divider": 0x3C if legacy_mode else 0x30,
                "vcxo_freq": Spll1Vco.VCO122_88MHz,
                "sysref_div": 1152,
                "clkin0_r_div": int(ref_clock_freq / pfd1),
                "pll1_n_div": 64,
                "pll2_prescaler": 2,
                "pll2_n_cal_div": 12,
                "pll2_n_div": 12,
                "prc_to_db": True,
            },
            3e9: {
                "ref_freq": ref_clock_freq,
                "output_freq": rfdc_freq,
                "output_divider": 1,
                "prc_divider": 0x3C if legacy_mode else 0x30,
                "vcxo_freq": Spll1Vco.VCO100MHz,
                "sysref_div": 1200,
                "clkin0_r_div": int(ref_clock_freq / pfd1),
                "pll1_n_div": 50,
                "pll2_prescaler": 3,
                "pll2_n_cal_div": 10,
                "pll2_n_div": 10,
                "prc_to_db": True,
            },
            3.072e9: {
                "ref_freq": ref_clock_freq,
                "output_freq": rfdc_freq,
                "output_divider": 1,
                "prc_divider": 0x3C if legacy_mode else 0x30,
                "vcxo_freq": Spll1Vco.VCO122_88MHz,
                "sysref_div": 1200,
                "clkin0_r_div": int(ref_clock_freq / pfd1),
                "pll1_n_div": 64,
                "pll2_prescaler": 5,
                "pll2_n_cal_div": 5,
                "pll2_n_div": 5,
                "prc_to_db": True,
            },
        }[rfdc_freq]
        spll_config = SpllConfig(**spll_args)
        resampling_factor = self.master_to_sample_clk[mcr][2]
        rfdc_config = RfdcConfig(conv_rate=rfdc_freq, resampling=resampling_factor)
        return X4xxClockConfig(
            spll_config=spll_config, rfdc_configs=[rfdc_config, rfdc_config], mmcm_use_defaults=True
        )

    def should_reboot_on_reconfiguration(self):
        """MPM reboot is not necessary for X410."""
        return False


class X440ClockPolicy(X4xxClockPolicy):
    """This is the clocking policy for X440.

    In contrast to the clocking policy of the X410 it uses the RFDC PLL
    and MMCM and it uses the flexibility of the LMK04832 much more.
    """

    # This is the lowest value possible and should therefore be
    # achievable with every bitfile. This value will only used
    # if we don't have information about the DSP bandwidth of
    # the bitfile available, otherwise the lookup table
    # `bandwidth_to_default_mcr` will be used.
    DEFAULT_MASTER_CLOCK_RATE = 125e6

    # Lookup table for setting up the correct master clock rate
    # depending on the DSP bandwidth (in MHz) of the FPGA image.
    bandwidth_to_default_mcr = {
        200: 245.76e6,
        400: 368.64e6,
        1600: 368.64e6,
    }

    def __init__(self, mboard_info, dboard_infos, args, log):
        """Initialize the X440 clock policy."""
        self.log = log.getChild("Clk_Policy")
        self._initial_args = args
        self.args = args
        self.conv_rates = None
        self._read_converter_rates()

        self._dsp_info = None
        # Use default values during startup
        self._dsp_bw = None
        self._spc = 8
        self._extra_resampling = 1
        self._valid_sysref_freqs = list(
            sysref_setting["SYSREF_FREQ"]
            for vcxo in LMK04832X4xx.SYSREF_CONFIG.keys()
            for sysref_setting in LMK04832X4xx.SYSREF_CONFIG[vcxo]
        )

    def set_dsp_info(self, dsp_info):
        """Store the DSP info of the current FPGA image."""
        self._dsp_info = dsp_info
        if self._dsp_info[0]["num_rx_chans"] == 0 and self._dsp_info[0]["num_tx_chans"] == 0:
            return

        # this part of DSP initialization is only relevant for radio frontends
        self._dsp_bw = dsp_info[0]["bw"]
        # We assume that SPC == the bitfile's spc_rx
        self._spc = dsp_info[0]["spc_rx"]
        # Assume that the SPC will always be the same for both DBs
        assert self._spc == dsp_info[1]["spc_rx"]
        self._extra_resampling = dsp_info[0]["extra_resampling"]
        # Assume that the extra resampling will always be the same for both DBs
        assert self._extra_resampling == dsp_info[1]["extra_resampling"]

    def get_default_mcr(self):
        """Return a reasonable default master clock rate."""
        if self._dsp_bw is None:
            return self.DEFAULT_MASTER_CLOCK_RATE
        if self.bandwidth_to_default_mcr.get(self._dsp_bw):
            return self.bandwidth_to_default_mcr.get(self._dsp_bw)
        raise AssertionError("Cannot determine default MCR.")

    def get_radio_clock_rate(self, mcr):
        """Return the radio clock rates for the current configuration."""
        return mcr / self._spc

    def get_num_rates(self):
        """Returns the number of different master clock rates we can handle."""
        return 2

    def validate_ref_clock_freq(self, ref_clock_freq, master_clock_rates):
        """Verify that ref_clock_freq is a valid reference clock frequency.

        Checks whether ref_clock_freq is valid for the given master clock rates.

        Will throw a RuntimeError if not.

        Does not modify the state of this policy.
        """
        master_clock_rates = self.coerce_mcr(master_clock_rates)
        config = self.get_config(ref_clock_freq, master_clock_rates)
        sysref_config = next(
            sysref_setting
            for sysref_setting in LMK04832X4xx.SYSREF_CONFIG[
                100e6 if config.spll_config.vcxo_freq == Spll1Vco.VCO100MHz else 122.88e6
            ]
            if sysref_setting["SYSREF_FREQ"]
            in self._find_sysref_matches(
                config.rfdc_configs[0].conv_rate / config.rfdc_configs[0].resampling
            )
        )

        step_size = sysref_config["PDF"]

        if ref_clock_freq % step_size != 0:
            raise RuntimeError("External reference clock frequency is of incorrect step size.")

    def _read_converter_rates(self):
        """Gets the converter rate.

        Gets the converter rate  from the argument string, if it is given there,
        None otherwise.
        """
        self.conv_rates = self.args.get("converter_rate")
        if self.conv_rates is None:
            return
        self.conv_rates = list(parse_multi_device_arg(self.args["converter_rate"], conv=float))
        if len(self.conv_rates) == 1:
            self.conv_rates = [self.conv_rates[0]] * self.get_num_rates()
        elif len(self.conv_rates) != self.get_num_rates():
            raise RuntimeError(
                f"Invalid number of converter rates provided! Must provide either a "
                f"single rate or {self.get_num_rates()} rates (one per daughterboard)."
            )

    def _match_bypass_freqs(self, mcrs):
        """Analyzes if the two chosen sample rates can be generated by using the RFDC PLL bypass."""
        use_pll_bypass = False

        # Check if both DBs require the same LMK VCO rate (otherwise we can return here already).
        common_vco_rates = self._get_common_lmk_vco_rates(mcrs)
        if (len(common_vco_rates)) == 0:
            return False, None, None
        # Now we can be sure we have a valid number here
        lmk_vco = int(common_vco_rates[0])
        # Calculate which LMK output rate is required to serve both DBs (least common multiple):
        required_rate = lcm(mcrs[0], mcrs[1])
        lmk_od = None
        if mcrs[0] == mcrs[1] or all(
            int(required_rate / mcr) in X4xxRfdcCtrl.RFDC_RESAMPLER for mcr in mcrs
        ):
            lmk_od = int(lmk_vco / required_rate)
            # Let the RFDC decimation do most work:
            for dec in X4xxRfdcCtrl.RFDC_RESAMPLER:
                if (
                    lmk_od % dec == 0
                    and X4xxRfdcCtrl.CONV_RATE_MIN
                    <= lmk_vco / (lmk_od / dec)
                    <= X4xxRfdcCtrl.CONV_RATE_MAX
                ):
                    lmk_od = int(lmk_od / dec)
                    # Keep using bypass
                    use_pll_bypass = True
                    break
                # Bypass not possible
                use_pll_bypass = False
        return use_pll_bypass, lmk_vco, lmk_od

    def _reduce_rfdc_frequency_list(self, resampling_factor):
        """Select RFDC frequencies compatible with given resampling factor.

        Filters the RFDC frequency list to only contain frequencies that can
        be supported with a valid SYSREF frequency.
        """
        return [
            rate
            for rate in RFDC_PLL_CONFIGS.keys()
            if len(self._find_sysref_matches(rate / resampling_factor)) > 0
        ]

    def _find_sysref_matches(self, mcr):
        """Search feasable SYSREF frequencies.

        Find all SYSREF frequencies that are compatible with the given
        converter rate and resampling factor.
        """
        return [
            sysref_rate
            for sysref_rate in self._valid_sysref_freqs
            if mcr / self._spc % sysref_rate == 0
        ]

    def _match_mcrs(self, desired_mcr, desired_conv_rate=0):
        """Coerces MCR and converter rate.

        With a given master clock rate and converter rate, check the value against
        what the RFDC is able to do and return the rounded values.
        """
        # Calculate desired converter rate: If a converter rate was passed, check if
        # we can decimate from converter rate to master clock rate and if this converter rate
        # is within the valid range. If this is impossible, pick the converter rate automatically.
        div = desired_conv_rate / desired_mcr
        coerced_mcr = desired_mcr
        if (
            div not in X4xxRfdcCtrl.RFDC_RESAMPLER
            or not X4xxRfdcCtrl.CONV_RATE_MIN <= desired_conv_rate <= X4xxRfdcCtrl.CONV_RATE_MAX
            or desired_mcr not in MCR_LMK_VCO
        ):
            coerced_mcr = min(sorted(MCR_LMK_VCO), key=lambda x: abs(x - desired_mcr))
            for div in sorted(X4xxRfdcCtrl.RFDC_RESAMPLER, reverse=True):
                if coerced_mcr * div <= X4xxRfdcCtrl.CONV_RATE_MAX:
                    break
        return coerced_mcr, coerced_mcr * div

    def _get_common_lmk_vco_rates(self, mcrs):
        """Returns a list of LMK VCO rates that can be used with both master clock rates."""
        vco_rates = [set(MCR_LMK_VCO[mcr]) for mcr in mcrs]
        return list(vco_rates[0].intersection(vco_rates[1]))

    def _get_common_spll_out_freqs(self, conv_rates, mcrs):
        """Calculates possible common LMK output frequencies to achieve the two converter rates."""
        # First get the required VCO rates for both DBs
        vco_rates = self._get_common_lmk_vco_rates(mcrs)
        # If we don't have common values, there is no common spll_out_freq, so we return early
        if len(vco_rates) == 0:
            return []

        # Get a list of all possible spll out frequencies for these converter rates
        spll_out_freqs = list(
            set.intersection(*[set(v) for k, v in RFDC_PLL_CONFIGS.items() if k in conv_rates])
        )
        # Then return them filtered for the VCO rates that are allowed for the MCRs
        return list(filter(lambda x: vco_rates[0] % x == 0, spll_out_freqs))

    def _get_max_mcr(self):
        """Returns the maximum sample rate based on the DSP BW reported by the FPGA."""
        # _dsp_bw is in MHz, multiplied by 1.28 for the sample rate necessary to capture this, e.g.
        # 1.6 GS/s (FPGA) * 1.28 = 2.048 GS/s (max sample rate)
        # 400 MS/s * 1.28 = 512 GS/s
        dsp_bw = self._dsp_bw
        if dsp_bw is None:
            # Play it safe and assume the lowest known dsp_bw
            dsp_bw = min(self.bandwidth_to_default_mcr.keys())
        return dsp_bw * 1e6 * 1.28

    def _get_min_mcr(self):
        """Returns the minimum sample rate allowed in X440."""
        return (
            X4xxRfdcCtrl.CONV_RATE_MIN / max(X4xxRfdcCtrl.RFDC_RESAMPLER) / self._extra_resampling
        )

    def _get_mmcm_rates(self, rfdc_rate, lmk_vco, sysref_rate):
        """Find MMCM config."""
        min_common_rfdc_rate = lcm(rfdc_rate[0], rfdc_rate[1])
        # Find the MMCM VCO rate that fits into the valid range and can serve both MCRs
        # Ceil operation for finding the first multiple of min_common_rfdc_rate that fits into range
        mmcm_vco_fit_factor = int(math.ceil(X4xxRfdcCtrl.MMCM_VCO_MIN / min_common_rfdc_rate))
        # Lowest potential MMCM VCO Rate
        min_mmcm_vco_rate = int(min_common_rfdc_rate * mmcm_vco_fit_factor)
        mmcm_cfg = {}
        for mmcm_vco_rate in range(
            min_mmcm_vco_rate, int(X4xxRfdcCtrl.MMCM_VCO_MAX + 1), min_common_rfdc_rate
        ):
            # Find the feedback divider and the PRC divider
            # MMCM input divider is always 1. Let's find the greatest possible numbers
            # both for the LMK PRC output divider and the feedback divider.
            # First get the GCD of LMK VCO and MMCM VCO (that's a possible MMCM input freq
            # but maybe too large)
            mmcm_input_max = math.gcd(int(lmk_vco), mmcm_vco_rate)
            mmcm_fb = int(mmcm_vco_rate / mmcm_input_max)
            mmcm_input = False
            # Might be that the calculated MMCM_FB doesn't fit the valid range, then try the next
            # possible MMCM VCO Rate immediately, otherwise check if a valid feedback divider can
            # be found that plays together nicely with the LMK VCO and the PRC divider.
            # Divide this mmcm_input until it fits into the MMCM input range:
            for div in range(mmcm_fb, X4xxRfdcCtrl.MMCM_FB_MAX + 1, mmcm_fb):
                mmcm_input = mmcm_vco_rate / div
                # MMCM input frequency needs to be in range and...
                if (
                    X4xxRfdcCtrl.MMCM_INPUT_MIN <= mmcm_input <= X4xxRfdcCtrl.MMCM_INPUT_MAX
                    and
                    # MMCM input frequency should be an integer value and...
                    not mmcm_input % 1
                    and
                    # the LMK PRC output divider should be an integer and...
                    not lmk_vco / mmcm_input % 1
                    and
                    # rc_div*2 is the fastest clock with smallest divider and that
                    # needs to be an integer, too:
                    not (mmcm_vco_rate / rfdc_rate[0] / 2) % 1
                    and not (mmcm_vco_rate / rfdc_rate[1] / 2) % 1
                    and
                    # RFDC rate must be a multiple of the PRC(==mmcm_input)
                    all([(rate % mmcm_input) == 0 for rate in rfdc_rate])
                    and
                    # PRC must be an integer multiple of SYSREF
                    mmcm_input % sysref_rate == 0
                ):
                    if not mmcm_cfg.get(mmcm_input) or mmcm_cfg.get(mmcm_input) > mmcm_vco_rate:
                        mmcm_cfg.update({mmcm_input: mmcm_vco_rate})
                mmcm_input = False
            if mmcm_input:
                break
        if len(mmcm_cfg.keys()) == 0:
            error_msg = (
                "Unable to find a valid MMCM configuration for Master Clock Rate(s)"
                ' requested. Refer to "About Sampling Rates and Master Clock Rates'
                ' for the USRP X440" in Knowledge Base for more information on'
                " supported rates."
            )
            self.log.error(error_msg)
            raise RuntimeError(error_msg)
        return max(mmcm_cfg), mmcm_cfg[max(mmcm_cfg)]

    def get_intermediate_clk_settings(self, ref_clk_freq, old_mcrs, new_mcrs):
        """Returns an intermediate clock settings object.

        An  intermediate object is necessary if going from the old to
        the new master clock rates would fail otherwise.
        """
        # TODO we can be smarter here -- not all transitions require this.
        if tuple(old_mcrs) != tuple(new_mcrs):
            return self.get_config(ref_clk_freq, [250e6, 250e6])
        return None

    def coerce_mcr(self, master_clock_rates):
        """Validate that the requested master clock rate is valid.

        Will throw a ValueError if not.

        Does not modify the state of this policy.
        """
        self._read_converter_rates()
        # We might get the MCR as tuple, so convert it to be on the safe side.
        master_clock_rates = list(master_clock_rates)
        if len(master_clock_rates) < self.get_num_rates():
            master_clock_rates = [master_clock_rates[0]] * self.get_num_rates()

        # Check if MCR meets BW
        for mcr in master_clock_rates:
            if not self._get_min_mcr() <= mcr <= self._get_max_mcr():
                raise ValueError(
                    f"Invalid master clock rate {mcr/1e6} MHz for current FPGA image "
                    f"with bandwidth {self._dsp_bw} MHz!"
                )

        # Check if we can generate these MCRs with the RFDC at all
        mcrs = []
        conv_rates = []
        for idx, mcr in enumerate(master_clock_rates):
            conv_rate = 0 if self.conv_rates is None else self.conv_rates[idx]
            mcr, cvr = self._match_mcrs(mcr, conv_rate)
            mcrs.append(mcr)
            conv_rates.append(cvr)
        # Find out if we can generate the two rates with the same LMK output frequency
        common_out = self._get_common_spll_out_freqs(conv_rates, mcrs)
        # Use the first MCR for both if we cannot combine them.
        if len(common_out) == 0:
            mcrs[1] = mcrs[0]
            # Ensure we're falling back to converter rate 0, too
            conv_rates[1] = conv_rates[0]

        # With these values check if we can do this in MMCM
        rfdc_rate = list(map(lambda x: int(x / (self._spc * self._extra_resampling)), mcrs))
        min_mmcm_vco_rate = lcm(rfdc_rate[0], rfdc_rate[1])
        # Inform user if we don't have an exact match
        if min_mmcm_vco_rate > X4xxRfdcCtrl.MMCM_VCO_MAX:
            mcrs[1] = mcrs[0]
            # Ensure we're falling back to converter rate 0, too
            conv_rates[1] = conv_rates[0]

        if mcrs != master_clock_rates:
            self.log.warning(
                f"Unable to use desired master clock rate(s), using "
                f"{mcrs[0]/1e6} MHz for DB0 and {mcrs[1]/1e6} MHz for DB1."
            )
        if self.conv_rates is not None and self.conv_rates != conv_rates:
            self.log.warning(
                f"Unable to use desired converter rate(s), using "
                f"{conv_rates[0]/1e6} MSps for DB0 "
                f"and {conv_rates[1]/1e6} MSps for DB1. Converter rate needs "
                f"to be a {X4xxRfdcCtrl.RFDC_RESAMPLER} multiple of the master "
                f"clock rates {mcrs[0]/1e6} MHz and {mcrs[1]/1e6} MHz "
                f"but additional clock constraints may limit this."
            )
        self.conv_rates = conv_rates
        return mcrs

    def get_config(self, ref_clock_freq, master_clock_rates):
        """Returns a valid configuration based on the master clock rate.

        It uses the configuration where the RFDC_CLOCK/SPC is the closest to the MCR
        This method is called after coerce_mcr() has run and - if necessary -
        rounded the MCR values, so will skip the checks here to save some time.
        """
        if len(master_clock_rates) != self.get_num_rates():
            master_clock_rates = [master_clock_rates[0]] * self.get_num_rates()

        # Get us the rounded mcr with fitting converter rates
        mcrs = []
        conv_rates = []
        for idx, mcr in enumerate(master_clock_rates):
            conv_rate = 0 if self.conv_rates is None else self.conv_rates[idx]
            mcr, cvr = self._match_mcrs(mcr, conv_rate)
            mcrs.append(mcr)
            conv_rates.append(cvr)
        # Check if we can bypass the RFDC PLL (lower phase noise)
        bypass, lmk_vco, lmk_od = self._match_bypass_freqs(mcrs)
        bypass_conv_rate = 0 if lmk_od is None else lmk_vco / lmk_od

        # Only bypass if this exact converter rate is desired
        if bypass and bypass_conv_rate in conv_rates:
            output_freq = lmk_vco / lmk_od
            self.log.info("Bypassing RFDC PLL")
            conv_rate = lmk_vco / lmk_od
            conv_rates = [conv_rate, conv_rate]
        else:
            # Get the common LMK output freq for both MCRs
            common_out = self._get_common_spll_out_freqs(conv_rates, mcrs)
            assert len(common_out) > 0
            self.log.info("Using RFDC PLL")
            # Use the maximum value
            output_freq = max(common_out)
            # Find out how to configure the LMK for this output freq:
            lmk_vco = self._get_common_lmk_vco_rates(mcrs)[0]
            lmk_od = int(lmk_vco / output_freq)
        self.conv_rates = conv_rates
        spll1_vco = 100e6 if lmk_vco % 100e6 == 0 else 122.88e6
        pll2_n = lmk_vco / spll1_vco
        pll2_prescaler = [x for x in LMK04832.PLL2_PRESCALER if not pll2_n % x]
        pll2_n_div = int(pll2_n / pll2_prescaler[0])

        # Looking for the correct sysref config needed for both MCRs
        sysref_config = next(
            sysref_setting
            for sysref_setting in LMK04832X4xx.SYSREF_CONFIG[spll1_vco]
            if all(sysref_setting["SYSREF_FREQ"] in self._find_sysref_matches(mcr) for mcr in mcrs)
        )

        # The following asserts are to explicitly check for SYSREF requirements
        # as per pg269, Ch. 4, Section "SYSREF Signal Requirements"
        assert sysref_config["SYSREF_FREQ"] < 10e6, "SysRef frequency exceeds limit of 10 MHz"
        assert all(
            conv_rate % sysref_config["SYSREF_FREQ"] == 0.0 for conv_rate in conv_rates
        ), "Converter Rate is not a multiple of the SysRef freq"

        rfdc_rate = list(map(lambda x: int(x / (self._spc * self._extra_resampling)), mcrs))
        mmcm_input, mmcm_vco_rate = self._get_mmcm_rates(
            rfdc_rate, lmk_vco, sysref_config["SYSREF_FREQ"]
        )
        prc_rate = mmcm_input

        if not mmcm_input or mmcm_input > lmk_vco:
            error_msg = (
                f"Unable to find a valid MMCM configuration for Master Clock Rate(s) "
                f"{mcrs[0]/1e6} MHz and {mcrs[1]/1e6} MHz. Please choose different "
                f"Master Clock Rate(s)."
            )
            self.log.error(error_msg)
            raise RuntimeError(error_msg)
        prc_div = int(lmk_vco / mmcm_input)
        assert prc_div in LMK04832X4xx.PRC_OUT_DIVIDERS, "Invalid LMK PRC output divider."
        mmcm_fb = int(mmcm_vco_rate / mmcm_input)
        assert (
            X4xxRfdcCtrl.MMCM_FB_MIN <= mmcm_fb <= X4xxRfdcCtrl.MMCM_FB_MAX
        ), "Invalid MMCM Feedback Divider."
        r0_clk = int(mmcm_vco_rate / rfdc_rate[0])
        assert (
            X4xxRfdcCtrl.MMCM_OD_MIN <= r0_clk <= X4xxRfdcCtrl.MMCM_OD_MAX
        ), "Invalid MMCM output divider for radio 0 clock."
        r1_clk = int(mmcm_vco_rate / rfdc_rate[1])
        assert (
            X4xxRfdcCtrl.MMCM_OD_MIN <= r1_clk <= X4xxRfdcCtrl.MMCM_OD_MAX
        ), "Invalid MMCM output divider for radio 1 clock."
        r0_clk2x = int(r0_clk / 2)
        assert (
            X4xxRfdcCtrl.MMCM_OD_MIN <= r0_clk2x <= X4xxRfdcCtrl.MMCM_OD_MAX
        ), "Invalid MMCM output divider for radio 0 clock 2x."
        r1_clk2x = int(r1_clk / 2)
        assert (
            X4xxRfdcCtrl.MMCM_OD_MIN <= r1_clk2x <= X4xxRfdcCtrl.MMCM_OD_MAX
        ), "Invalid MMCM output divider for radio 1 clock 2x."
        prc_out_div = int(mmcm_vco_rate / prc_rate)
        assert (
            X4xxRfdcCtrl.MMCM_OD_MIN <= prc_out_div <= X4xxRfdcCtrl.MMCM_OD_MAX
        ), "Invalid MMCM output divider for PRC."

        spll_cfg = {
            "ref_freq": ref_clock_freq,
            "output_freq": output_freq,
            "output_divider": lmk_od,
            "prc_divider": prc_div,
            "vcxo_freq": Spll1Vco.VCO100MHz if spll1_vco == 100e6 else Spll1Vco.VCO122_88MHz,
            "sysref_div": int(lmk_vco / sysref_config["SYSREF_FREQ"]),
            "clkin0_r_div": int(ref_clock_freq / sysref_config["PDF"]),
            "pll1_n_div": int(sysref_config["SYSREF_FREQ"] / sysref_config["PDF"]),
            "pll2_prescaler": int(pll2_prescaler[0]),
            "pll2_n_cal_div": pll2_n_div,
            "pll2_n_div": pll2_n_div,
            "prc_to_db": False,  # X440 does not have CPLD on DB, so turn off toggling PRC signal
        }
        rfdc_cfg0 = {"conv_rate": conv_rates[0], "resampling": int(conv_rates[0] / mcrs[0])}
        rfdc_cfg1 = {"conv_rate": conv_rates[1], "resampling": int(conv_rates[1] / mcrs[1])}
        clk_config = {
            "mmcm_use_defaults": False,
            "mmcm_feedback_divider": mmcm_fb,
            "mmcm_input_divider": 1,
            "mmcm_output_div_map": {
                "r0_clk": r0_clk,
                "r1_clk": r1_clk,
                "r0_clk_2x": r0_clk2x,
                "r1_clk_2x": r1_clk2x,
                "prc": prc_out_div,
            },
            "rfdc_configs": [RfdcConfig(**rfdc_cfg0), RfdcConfig(**rfdc_cfg1)],
            "spll_config": SpllConfig(**spll_cfg),
        }

        return X4xxClockConfig(**clk_config)

    def should_reboot_on_reconfiguration(self):
        """X4xx requires MPM restart after clock reconfiguration."""
        return True


def get_clock_policy(mboard_info, dboard_infos, args, log):
    """Return a clocking policy object based on the available hardware."""
    if dboard_infos[0]["pid"] in ZBX.pids:
        return X410ClockPolicy(mboard_info, dboard_infos, args, log)
    if dboard_infos[0]["pid"] in FBX.pids:
        return X440ClockPolicy(mboard_info, dboard_infos, args, log)
    raise RuntimeError("Could not resolve clock policy.")