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 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272
|
#
# Copyright 2021 Ettus Research, a National Instruments Company
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
"""
X4x0 Motherboard Clocking Management
This module handles the clocking on the X4x0 motherboard and the RFSoC. 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
(e.g., via eCPRI). The clocking aux board has its own control class which also
contains controls for the eCPRI PLL (ClockingAuxBrdControl).
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 for X410/ZBX) 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).
The SPLL 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. The
corresponding clock is generated in the FPGA using the PRC as a reference clock,
using an MMCM.
For a block diagram, cf. usrp_x4xx.dox. For more details, see the schematic.
"""
from enum import Enum
from usrp_mpm.mpmutils import parse_multi_device_arg, str2bool
from usrp_mpm.periph_manager.x4xx_clk_aux import ClockingAuxBrdControl
from usrp_mpm.periph_manager.x4xx_clock_types import RpllRefSel, BrcSource
from usrp_mpm.periph_manager.x4xx_clock_ctrl import X4xxClockCtrl
from usrp_mpm.rpc_utils import no_rpc
class X4xxClockManager:
"""
X4xx Clock Manager: This class handles all the clocking-related features of
the X4xx. It relies on other classes:
- X4xxClockCtrl: This class contains the actual knobs to twiddle, controls
for the various PLLs, etc.
- X4xxClockPolicy: This is a device-specific class (e.g., it would be a
different class for X410 than something else). It
encodes all the various settings for the different
clocking settings.
- X4xxRfdcCtrl: This is for controlling the RFDC. Unlike the first two
classes, this is passed in, and not exclusively owned by this
clock manager object.
This class implements many of the public MPM APIs (meaning, some RPC calls
made from UHD go directly into this class).
The one clocking-related thing not controlled by this class is the
behaviour of the TRIG-IO connector on the X4xx back panel (it is controlled
in the main x4xx class).
For more details on the clocking management, refer to the module docstring.
"""
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),
}
X400_DEFAULT_TIME_SOURCE = TIME_SOURCE_INTERNAL
X400_DEFAULT_CLOCK_SOURCE = CLOCK_SOURCE_INTERNAL
# 25 MHz is the default BRC provided by the RPLL
X400_DEFAULT_INT_CLOCK_FREQ = 25e6
# With no other info, we assume that the user will be providing a 10 MHz
# external reference clock
X400_DEFAULT_EXT_CLOCK_FREQ = 10e6
# 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
# This is the fixed oscillator used as a reliable input to the RPLL
X400_RPLL_SECREF_RATE = 100e6
X400_DEFAULT_MGT_CLOCK_RATE = 156.25e6
X400_MIN_EXT_REF_FREQ = 1e6
X400_MAX_EXT_REF_FREQ = 50e6
class SetSyncRetVal(Enum):
""" Return value options for _set_sync_source() """
OK = 'OK'
NOP = 'nop'
FAIL = 'failure'
def __init__(self,
args,
clk_policy,
clk_aux_board,
cpld_control,
log):
self.log = log.getChild("ClkMgr")
self._clocking_auxbrd = clk_aux_board
# Number of daughterboards (set later from RFDC CTRL)
self.num_dboards = 2
# Tasks to be executed by the host if queried
self.tasks = {}
self.skip_adc_selfcal = False
if self._clocking_auxbrd:
self._safe_sync_source = {
'clock_source': self.X400_DEFAULT_CLOCK_SOURCE,
'time_source': self.X400_DEFAULT_TIME_SOURCE,
'skip_mpm_reboot': 1,
}
else:
self._safe_sync_source = {
'clock_source': self.CLOCK_SOURCE_MBOARD,
'time_source': self.TIME_SOURCE_INTERNAL,
'skip_mpm_reboot': 1,
}
self.clk_policy = clk_policy
# Parse args
self._time_source = args.get(
'time_source', self._safe_sync_source['time_source'])
self._clock_source = args.get(
'clock_source', self._safe_sync_source['clock_source'])
if not self._clocking_auxbrd:
self._clock_source = self.CLOCK_SOURCE_MBOARD
# This is the reference clock frequency generated by the motherboard
# itself (from the RPLL)
self._int_clock_freq = self.X400_DEFAULT_INT_CLOCK_FREQ
# Here, we only allow a scalar master_clock_rate value, even though the
# device may support multiple rates. However, those need to be set in
# init(). Note that the clock policy doesn't know anything yet other than
# the EEPROM contents (i.e., it knows which device we're on) because
# without clocks, we can't read from the FPGA registers. The default
# MCR thus is going to be a rough guess, at best.
self._master_clock_rates = [
float(args.get('master_clock_rate', self.clk_policy.get_default_mcr()))]
# This is the reference clock frequency that comes from the clocking
# aux board
self._ext_clock_freq = None
self._set_ref_clock_freq(
float(args.get('ext_clock_freq', self.X400_DEFAULT_EXT_CLOCK_FREQ)),
self._master_clock_rates,
update_clocks=False)
# If there is a valid PRIREF clock signal available at the RPLL (e.g.,
# when SyncE is being used), set this to whatever the rate of the input
# clock signal is.
# Note: The secondary reference (SECREF) is always enabled, it's a
# fixed, reliable 100 MHz oscillator.
self._rpll_priref_rate = None
# These need to be added later (when __init__ is called, the call site
# does not yet have these available, because it needs to init some
# clocks for that)
self.rfdc = None
self._set_reset_db_clocks = lambda x: None
# Init peripherals
self._init_available_srcs()
if self._time_source not in self.get_time_sources():
raise ValueError(f"Invalid time source configured: {self._time_source}")
if self._clock_source not in self.get_clock_sources():
raise ValueError(f"Invalid clock source configured: {self._clock_source}")
self.clk_ctrl = X4xxClockCtrl(cpld_control, log)
self._init_ref_clock_and_time()
self._init_meas_clock()
self.configured_since_boot = False
self.tasks["mpm_reboot"] = []
### IMPORTANT! The clocking initialization is not complete until
### finalize_init() was called. This is as far as we get for now.
###########################################################################
# 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(f"Available time sources are: {self._avail_time_sources}")
def _init_ref_clock_and_time(self):
"""
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.
"""
# 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'])
# This is a pared-down version of set_sync_source(), but without
# configuring any clocks.
self._set_brc_source(self.get_clock_source(), self.get_time_source())
# Now configure the PLLs
self._config_rpll(
self.X400_DEFAULT_MGT_CLOCK_RATE,
self.X400_DEFAULT_INT_CLOCK_FREQ,
RpllRefSel.SECONDARY)
clk_config = self.clk_policy.get_config(
self.get_ref_clock_freq(), self._master_clock_rates)
self.clk_ctrl.config_spll(clk_config.spll_config)
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.trace("TDC measurement clock not yet implemented.")
@no_rpc
def finalize_init(self, args, mboard_regs_control, rfdc):
"""
Store a reference to the RFdc object, and update internal state.
This completes initialization of the clocking. After this call, all
clocks are ticking at a useful rate.
"""
self.clk_ctrl.mboard_regs_control = mboard_regs_control
self.rfdc = rfdc
# Update the policy with new info on the FPGA image:
self.clk_policy.set_dsp_info(self.rfdc.get_dsp_info())
self.num_dboards = len(self.rfdc.get_dsp_info())
for db in range(self.num_dboards):
self.tasks[f"db{db}_ADCSelfCal"] = []
# Now do the full MCR init for the first time. When this is done, all
# the clocks will be ticking. Now, the policy should know about the
# FPGA capabilities, and can choose the correct default MCR.
initial_mcr = \
float(args.get('master_clock_rate', self.clk_policy.get_default_mcr()))
try:
initial_mcr = self.clk_policy.coerce_mcr([initial_mcr])
except ValueError:
self.log.warning(
f"Requested initial master clock rate {initial_mcr/1e6} MHz is invalid!")
initial_mcr = self.clk_policy.get_default_mcr()
self._master_clock_rates = initial_mcr
# Force reset the RFDC to ensure it is in a good state. Note that for
# pulling the RFDC out of reset, we need access to the rfdc object, and
# the clocking policy needs access to the DSP info, meaning this is the
# earliest point during initialization when we can do this reset.
self._reset_clocks(True, ('mmcm', 'rfdc'))
self._reset_clocks(False, ('mmcm', 'rfdc'))
# Devices that use non-default clocking parameters may require an explicit
# startup after the RFDC reset:
self.rfdc.startup_tiles(quiet=True)
# The initial default mcr only works if we have an FPGA with
# a decimation of 2. But we need the overlay applied before we
# can detect decimation, and that requires clocks to be initialized.
self.set_master_clock_rate([initial_mcr[0],])
@no_rpc
def set_dboard_reset_cb(self, db_reset_cb):
"""
Provide callbacks to set/reset daughterboard clocks. The controls for
these live elsewhere (not on X4xxClockCtrl), thus they have to be given
to us after the dboards have been initialized.
"""
self._set_reset_db_clocks = db_reset_cb
@no_rpc
def init(self, args):
"""
Called by x4xx.init(), when a UHD session initializes.
Rules for argument handling:
- If clock_source and/or time_source are not given, then we use the
defaults (internal)
- If master_clock_rate is not given, we use whatever the device last
was set to.
- If ext_clock_freq is not given, we use whatever the device was last
set to (this only matters for 'external' clock source!)
- If converter_rate is not given, we use the maximum possible converter
rate. If it is given, but master_clock_rate is not given, we ignore it.
- If clock source, time source, ext_clock_freq, and master clock rate do
not change from run to run, we skip re-initializing the RFDC chain.
- The user can override this by specifying `force_reinit` (like with
N310).
- If converter_rate is given, we always force reinit. This could be
optimized away if desired.
"""
args['clock_source'] = args.get('clock_source', self.X400_DEFAULT_CLOCK_SOURCE)
args['time_source'] = args.get('time_source', self.X400_DEFAULT_TIME_SOURCE)
self.clk_policy.args = args
args['initializing'] = True
# This flag is used to skip the self-cal that otherwise is marked as required
# after each clocking change
self.skip_adc_selfcal = args.get('skip_adc_selfcal', False)
# This flag will be used to force a full run of the clocking initialization.
# If False, MPM may still decide to do a full clocking initialization,
# depending on other settings.
force_set_mcr = bool(args.get('force_reinit', False))
# If this is None, the user desires the MCR to not change
desired_master_clock_rates = args.get('master_clock_rate')
desired_converter_rates = args.get('converter_rate')
# If the user specifies everything, then we always force setting
# everything. This can be optimized in the future.
force_set_mcr = desired_master_clock_rates and desired_converter_rates
# Convert desired master clock rate from string into a tuple. We cover
# the special case where a single rate was given to populate both
# daughterboard rates.
if desired_master_clock_rates:
desired_master_clock_rates = \
parse_multi_device_arg(args['master_clock_rate'], conv=float)
if len(desired_master_clock_rates) == 1 and \
len(self._master_clock_rates) > 1:
desired_master_clock_rates = \
desired_master_clock_rates * len(self._master_clock_rates)
if 'ext_clock_freq' in args:
new_ext_clock_freq = float(args.get('ext_clock_freq'))
if new_ext_clock_freq != self._ext_clock_freq:
# This will update self._ext_clock_freq and validate, but not
# apply the settings to hardware.
self._set_ref_clock_freq(
new_ext_clock_freq,
desired_master_clock_rates \
if desired_master_clock_rates \
else self._master_clock_rates,
update_clocks=False)
# Now we force an update to all clocking further down.
if args['clock_source'] == self.CLOCK_SOURCE_EXTERNAL:
if not desired_master_clock_rates:
desired_master_clock_rates = self._master_clock_rates
force_set_mcr = True
# set_sync_source() will cause a reconfiguration of the master clock rate
# if anything changed. By modifying self._master_clock_rates, we can
# use that to immediately set the sync source, the external ref clock freq,
# and the MCR all in one go. If we called set_master_clock_rate()
# separately, there's a theoretical chance that we would fail to lock
# the old MCR with the new ext_clock_freq, or vice versa.
if desired_master_clock_rates:
args['master_clock_rate'] = desired_master_clock_rates
if desired_converter_rates and not desired_master_clock_rates:
self.log.warning("Passing `converter_rate` argument without obligatory "
"`master_clock_rate` argument. Ignoring `converter_rate`.")
if force_set_mcr:
args['force_reinit'] = True
self.set_sync_source(args)
if not 'boot_init' in args:
self.configured_since_boot = True
@no_rpc
def deinit(self):
"""
Called by x4xx.deinit(), when a UHD session terminates.
This will put clocks etc. into a valid state, if they previously weren't
in one.
"""
if not self.clk_ctrl.get_ref_locked():
self.log.error("ref clocks aren't locked, falling back to default")
self.set_sync_source(self._safe_sync_source)
@no_rpc
def tear_down(self):
"""
Called by x4xx.tear_down(), when the device driver is being unloaded.
"""
self.rfdc = None
self._set_reset_db_clocks = None
###########################################################################
# Internal helpers/workers
###########################################################################
def _set_sync_source(self, clock_source, time_source, force=False):
"""
See set_sync_source() for docs. This is the internal helper that does
the actual work, but it assumes clock_source and time_source are
validated/sanitized.
This method checks if the new clock/time source are different from the
current ones. If not, it will return SetSyncRetVal.NOP and not touch
the hardware.
If anything changed, then all clocks will be put into reset. Call
set_master_clock_rate() in that case to get them running again!
"""
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 not force and \
clock_source == self._clock_source and \
time_source == self._time_source:
if self.clk_ctrl.get_ref_locked():
# Nothing changed, 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...")
# 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(True, ('db_clock', 'cpld', 'rfdc', 'mmcm', 'spll'))
self._set_brc_source(clock_source, time_source)
self._clock_source = clock_source
return self.SetSyncRetVal.OK
def _config_rpll(
self,
usr_clk_rate: float,
internal_brc_rate: float,
internal_brc_source: RpllRefSel):
"""
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
(primary or secondary). Primary is the clocking
aux card, secondary is the 100 MHz oscillator.
"""
assert isinstance(internal_brc_source, RpllRefSel)
if internal_brc_source == RpllRefSel.PRIMARY and \
self._rpll_priref_rate is None:
err = f"Invalid internal BRC source '{internal_brc_source}' was selected."
self.log.error(err)
raise RuntimeError(err)
ref_rate = self._rpll_priref_rate \
if internal_brc_source == RpllRefSel.PRIMARY \
else self.X400_RPLL_SECREF_RATE
self.clk_ctrl.config_rpll(
internal_brc_source,
ref_rate,
internal_brc_rate,
usr_clk_rate)
# 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_mmcm(self, clk_settings):
"""
Configure the MMCM which provides various internal clocks to the
data path after the RFdc.
Also waits for the MMCM to be done with reconfiguration.
"""
self.log.trace("Entering _config_mmcm()")
assert self.rfdc
# First configure all MMCM settings:
if not clk_settings.mmcm_use_defaults:
self.log.debug("Updating MMCM")
# This reconfigures, but does not wait for a valid lock.
self.rfdc.rfdc_configure_mmcm(
clk_settings.mmcm_input_divider,
clk_settings.mmcm_feedback_divider,
clk_settings.mmcm_output_div_map)
self.log.trace('MMCM Configuration applied, now waiting for MMCM to lock...')
self.rfdc.reset_mmcm(reset=False)
else:
self.log.debug("Using default values for MMCM")
def _set_ref_clock_freq(self, freq, master_clock_rates, update_clocks=True):
"""
Tell our USRP what the frequency of the external reference clock is.
Will throw if it's not a valid value.
"""
if self._ext_clock_freq == freq:
return
if freq < self.X400_MIN_EXT_REF_FREQ or freq > self.X400_MAX_EXT_REF_FREQ:
raise RuntimeError(
'External reference clock frequency is out of the valid range.')
self.clk_policy.validate_ref_clock_freq(freq, master_clock_rates)
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 update_clocks and self.get_clock_source() == self.CLOCK_SOURCE_EXTERNAL:
self.set_sync_source({'clock_source': self._clock_source,
'time_source': self._time_source})
def _set_brc_source(self, clock_source, time_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:
if self._clocking_auxbrd:
self._clocking_auxbrd.export_clock(False)
self.clk_ctrl.select_brc_source(BrcSource.RPLL)
else:
if self._clocking_auxbrd is None:
self.log.error(f'Invalid BRC selection {clock_source}. No clocking aux '
'board was found.')
raise RuntimeError(f'Invalid BRC selection {clock_source}')
self.clk_ctrl.select_brc_source(BrcSource.CLK_AUX)
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 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(f'Invalid BRC selection {clock_source}')
raise RuntimeError(f'Invalid BRC selection {clock_source}')
self.log.debug(f"Base reference clock source is: {clock_source}")
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(f"Reset clocks: {reset_list}")
if 'db_clock' in reset_list:
self._set_reset_db_clocks(value)
if 'cpld' in reset_list:
self.clk_ctrl.reset_clock(value, 'cpld')
if 'rfdc' in reset_list:
assert self.rfdc
self.rfdc.reset_rfdc(reset=value)
if 'mmcm' in reset_list:
self.rfdc.reset_mmcm(reset=value)
if 'spll' in reset_list:
self.clk_ctrl.reset_clock(value, 'spll')
if 'rpll' in reset_list:
self.clk_ctrl.reset_clock(value, 'rpll')
else:
self.log.trace(f"Bring clocks out of reset: {reset_list}")
# Inverse order from resetting
if 'rpll' in reset_list:
self.clk_ctrl.reset_clock(value, 'rpll')
if 'spll' in reset_list:
self.clk_ctrl.reset_clock(value, 'spll')
if 'mmcm' in reset_list:
assert self.rfdc
self.rfdc.reset_mmcm(reset=value)
if 'rfdc' in reset_list:
assert self.rfdc
self.rfdc.reset_rfdc(reset=value)
if 'cpld' in reset_list:
self.clk_ctrl.reset_clock(value, 'cpld')
if 'db_clock' in reset_list:
self._set_reset_db_clocks(value)
def _configure_clock_chain(self, clk_settings, time_source, ref_clk_freq):
"""
Configures clocking chain (RPLL -> SPLL -> MMCM) with given clock
settings. This function does not reset the RFDC, so that its
startup can be controlled independently.
This configuration takes 1-3 seconds, depending on the configuration
and the previous state of the clocks.
Arguments:
clk_settings -- A clock settings object to be applied.
time_source -- The current time source
"""
# Reset everything downstream from SPLL
self._reset_clocks(True, ('mmcm', 'rfdc', 'cpld', 'db_clock'))
# The following call will return only when the SPLL successfully locks
# to the new settings:
self.clk_ctrl.config_spll(clk_settings.spll_config)
# When the SPLL is configured and locked, its output dividers are
# synchronized (share a common flank). Next, we need to synchronize the
# R-dividers to the common PPS signals. Because we need to wait for the
# PPS, this function may take > 1s to execute, worst-case.
self.clk_ctrl.sync_spll_clocks(
"internal_pps" if time_source == self.TIME_SOURCE_INTERNAL else "external_pps",
ref_clk_freq)
# At this point the SPLL is sync'd in time and frequency to the reference.
# From now on, no-one will be touching the SPLL until we call
# set_master_clock_rate() again.
# Bring MMCM out of reset, and reconfigure. The MMCM lock status
# is monitored at the end of the MMCM DRP access, as it is required
# for the MMCM to report the DRP configuration as complete.
self.rfdc.reset_mmcm(reset=False, check_locked=False)
self.rfdc.rfdc_update_mmcm_regs()
self._config_mmcm(clk_settings)
###########################################################################
# Public APIS, but not MPM APIs (these can be called by x4xx or the
# DB iface)
###########################################################################
@no_rpc
def set_master_clock_rate(self, master_clock_rates):
"""
Sets the master clock rate by configuring the RFDC decimation and SPLL,
and then resetting downstream clocks.
Arguments:
master_clock_rates -- An array either of length 1 (then this rate will be
applied to all daughterboards) or of length equal
to the number of daughterboards, if the current
design supports separate clock rates per dboard.
If the design only supports a single rate, then it
must be of length 1.
A scalar value will be interpreted as a list of
length 1.
"""
master_clock_rates = master_clock_rates \
if isinstance(master_clock_rates, (list, tuple)) \
else [master_clock_rates]
max_num_rates = self.clk_policy.get_num_rates()
if len(master_clock_rates) == 1:
master_clock_rates = [master_clock_rates[0]] * max_num_rates
elif max_num_rates == 1:
# User provided > 1 rate, but we can only support one
raise RuntimeError(
'Invalid number of clock rates provided! Must provide a single rate.')
elif len(master_clock_rates) != max_num_rates:
raise RuntimeError(
f'Invalid number of clock rates provided! Must provide either a '
f'single rate or {max_num_rates} rates.')
# Get the validated and rounded MCR back
master_clock_rates = self.clk_policy.coerce_mcr(master_clock_rates)
clk_settings = self.clk_policy.get_config(
self.get_ref_clock_freq(), master_clock_rates)
self.log.debug(f"Clock Config: {clk_settings}")
self.log.info(f"Using Clock Configuration:\n"
f"DB0: Master Clock Rate: {master_clock_rates[0]/1e6} MSps "
f"@Converter Rate {clk_settings.rfdc_configs[0].conv_rate/1e9} GHz\n"
f"DB1: Master Clock Rate: {master_clock_rates[-1]/1e6} MSps "
f"@Converter Rate {clk_settings.rfdc_configs[1].conv_rate/1e9} GHz")
if clk_settings.rfdc_configs[1].conv_rate > clk_settings.rfdc_configs[0].conv_rate:
self.log.warn('Converter Rate 1 is larger than Converter Rate 0. This will impact '
' RF performance. Consider swapping your master clock rate values.')
self._configure_clock_chain(
clk_settings, self.get_time_source(), self.get_ref_clock_freq())
# Bring RFDC out of reset, reset tiles and reconfigure RFDC
self._reset_clocks(False, ('rfdc',))
self.rfdc.reset_tiles()
self.rfdc.configure(clk_settings.spll_config.output_freq, clk_settings.rfdc_configs)
# If non-default clocking parameters are used, reset_tiles() may not be successful in
# the first place, so we have to start the tiles explicitely again after the
# reconfiguration:
self.rfdc.startup_tiles()
# According to PG269, self-cal coefficients get lost when calling startup_tiles().
# Therefore we need to mark self-cal as required again after that. May be skipped
# with 'skip_adc_selfcal' argument (but should be called manually then).
if not self.skip_adc_selfcal:
for db in range(self.num_dboards):
self.tasks[f"db{db}_ADCSelfCal"] = [{"Run":"True"}]
# All settings are applied: Now bring other clocks out of reset. Note
# that cpld and db_clock don't depend on MMCM or RFDC, but we don't need
# them so they only come back now.
self._reset_clocks(False, ('cpld', 'db_clock'))
self._master_clock_rates = master_clock_rates
# Configure PPS forwarding to timekeepers. The requirement is that this
# be called after sync_spll_clocks() was called.
for tk_idx, mcr in enumerate(master_clock_rates):
self.clk_ctrl.configure_pps_forwarding(tk_idx, True,
self.clk_policy.get_radio_clock_rate(mcr))
@no_rpc
def get_master_clock_rate(self, db_idx=0):
""" Return the master clock rate set during init """
return self._master_clock_rates[0] \
if len(self._master_clock_rates) == 1 \
else self._master_clock_rates[db_idx]
@no_rpc
def get_ref_clock_freq(self):
"""
Returns the currently active reference clock frequency (BRC).
Note: We don't measure this frequency, we need to be told what it is.
This happens during __init__() or init().
If the value that is passed into here and the actual reference clock
frequency don't match, then things may fail.
"""
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 self.X400_GPSDO_OCXO_CLOCK_FREQ
# clock_source == "external":
return self._ext_clock_freq
@no_rpc
def get_converter_rate(self, db_idx):
"""
Returns the rate at which the ADC/DAC on a given daughterboard is
sampling.
"""
if not self.rfdc:
self.log.info(
"Querying converter rate from SPLL, RFDC is not yet enabled!")
return self.clk_ctrl.get_spll_freq()
return self.rfdc.get_converter_rate(db_idx)
@no_rpc
def get_prc_rate(self):
"""
Return the PRC rate
"""
return self.clk_ctrl.get_prc_rate()
###########################################################################
# Clock/Time API (this is a public MPM API)
###########################################################################
def get_spll_freq(self):
"""
Return the output frequency of the SPLL that is routed to the RFDC PLLs.
Note: To get the actual converter rate, don't use this. Instead, use
get_dboard_sample_rate().
"""
return self.clk_ctrl.get_spll_freq()
def get_clock_source(self):
" Return the currently selected clock source "
return self._clock_source
def get_clock_sources(self):
"""
Lists all available clock sources.
"""
return self._avail_clk_sources
def set_clock_source(self, *args):
"""
Ensures the new reference clock source and current time source pairing
is valid and sets both by calling set_sync_source().
"""
clock_source = args[0]
time_source = self.get_time_source()
assert clock_source is not None
assert time_source is not None
if (clock_source, time_source) not in self.valid_sync_sources:
old_time_source = time_source
if clock_source in (
self.CLOCK_SOURCE_MBOARD, self.CLOCK_SOURCE_INTERNAL):
time_source = self.TIME_SOURCE_INTERNAL
elif clock_source == self.CLOCK_SOURCE_EXTERNAL:
time_source = self.TIME_SOURCE_EXTERNAL
elif clock_source == self.CLOCK_SOURCE_GPSDO:
time_source = self.TIME_SOURCE_GPSDO
self.log.warning(
f"Time source '{old_time_source}' is an invalid selection with "
f"clock source '{clock_source}'. "
f"Coercing time source to '{time_source}'")
self.set_sync_source({
"clock_source": clock_source, "time_source": time_source})
def set_clock_source_out(self, enable):
"""
Allows routing the clock configured as source on the clk aux board to
the RefOut terminal. This only applies to internal, gpsdo and nsync.
"""
if self._clocking_auxbrd is None:
raise RuntimeError("No clocking aux board available")
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}'")
return self._clocking_auxbrd.export_clock(enable)
def get_time_source(self):
" Return the currently selected time source "
return self._time_source
def get_time_sources(self):
" Returns list of valid time sources "
return self._avail_time_sources
def set_time_source(self, time_source):
"""
Set a time source
This will call set_sync_source() internally, and use the current clock
source as a clock source. If the current clock source plus the requested
time source is not a valid combination, it will coerce the clock source
to a valid choice and print a warning.
"""
clock_source = self.get_clock_source()
assert clock_source is not None
assert time_source is not None
if (clock_source, time_source) not in self.valid_sync_sources:
old_clock_source = clock_source
if time_source == self.TIME_SOURCE_QSFP0:
clock_source = self.CLOCK_SOURCE_MBOARD
elif time_source == self.TIME_SOURCE_INTERNAL:
clock_source = self.CLOCK_SOURCE_MBOARD
elif time_source == self.TIME_SOURCE_EXTERNAL:
clock_source = self.CLOCK_SOURCE_EXTERNAL
elif time_source == self.TIME_SOURCE_GPSDO:
clock_source = self.CLOCK_SOURCE_GPSDO
self.log.warning(
f"Clock source {old_clock_source} is an invalid selection with "
f"time source {time_source}. Coercing clock source to {clock_source}")
self.set_sync_source(
{"time_source": time_source, "clock_source": clock_source})
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]
def set_sync_source(self, args):
"""
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 self.rfdc
# Check the clock source, time source, and combined pair are valid:
clock_source = args.get('clock_source', self.get_clock_source())
if clock_source not in self.get_clock_sources():
raise ValueError(f'Clock source {clock_source} is not a valid selection')
time_source = args.get('time_source', self.get_time_source())
if time_source not in self.get_time_sources():
raise ValueError(f'Time source {time_source} is not a valid selection')
if (clock_source, time_source) not in self.valid_sync_sources:
raise ValueError(
f'Clock and time source pair ({clock_source}, {time_source}) is '
'not a valid selection')
# Now figure out which master clock rate we'll be switching to. It might
# be the same as before.
master_clock_rates = args.get('master_clock_rate', self._master_clock_rates)
if isinstance(master_clock_rates, str):
master_clock_rates = parse_multi_device_arg(
master_clock_rates, conv=float)
mcr_change = tuple(master_clock_rates) != tuple(self._master_clock_rates)
# Also check the external reference clock frequency is valid
if clock_source == self.CLOCK_SOURCE_EXTERNAL:
self.clk_policy.validate_ref_clock_freq(
self.get_ref_clock_freq(), master_clock_rates)
# Sanity checks complete. Now check if we need to disable the RefOut.
# Reminder: RefOut and PPSIn share an SMA. Besides, you can't export an
# external clock. We are thus not checking for time_source == 'external'
# because that's a subset of clock_source == 'external'.
# We also disable clock exports for 'mboard', because the mboard clock
# does not get routed back to the clocking aux board and thus can't be
# exported either.
if clock_source in (self.CLOCK_SOURCE_EXTERNAL, self.CLOCK_SOURCE_MBOARD) \
and self._clocking_auxbrd:
self._clocking_auxbrd.export_clock(enable=False)
# Now configure the sync sources:
force_update = str2bool(args.get('force_reinit', False)) or mcr_change
ret_val = self._set_sync_source(clock_source, time_source, force_update)
if ret_val == self.SetSyncRetVal.NOP and not force_update:
self.log.debug("Skipping reconfiguration of clocks.")
return
try:
# An intermittent spur has been seen on multiple reconfigurations of clocking for x440.
# If we have already reconfigured clocking after initializtion, the next clocking
# reconfiguration will trigger MPM to reboot
skip_mpm_reboot = str2bool(args.get('skip_mpm_reboot', False))
initializing = str2bool(args.get('initializing', False))
if self.clk_policy.should_reboot_on_reconfiguration() \
and self.configured_since_boot \
and not skip_mpm_reboot:
if initializing:
self.tasks["mpm_reboot"] = [{"Run":"True"}]
# We are going to reboot mpm, so return early
return
else:
# If a different clocking configuration is being set through an
# API after device initialization (e.g. set_clock_source()), then
# don't return and reboot mpm, throw an error that reconfiguring
# the clocking configuration can lead to bad performance
raise RuntimeError("Changing clocking configuration after device "
"initialization is not permitted since it can "
"lead to decreased spurious performance. "
"To configure clocking settings, use the proper "
"device arguments during initialization")
if skip_mpm_reboot:
self.log.warning("Overriding recommended MPM reboot during clocking configuration. "
"This can lead to decreased spurious performance")
self.log.debug(
f"Reconfiguring clock configuration for a master clock rate of "
f"{master_clock_rates}")
# Re-set master clock rate. If this doesn't work, it will time out
# and throw an exception. We need to put the device back into a safe
# state in that case.
interm_clk_settings = self.clk_policy.get_intermediate_clk_settings(
self.get_ref_clock_freq(),
self._master_clock_rates,
master_clock_rates)
if interm_clk_settings:
self.log.debug( "Applying intermediate clock settings.")
self._configure_clock_chain(interm_clk_settings,
self.get_time_source(),
self.get_ref_clock_freq())
# Note that set_master_clock_rate() will also configure the
# clock chain, so if we had an intermediate settting, it will
# be overwritten immediately.
self.set_master_clock_rate(master_clock_rates)
# Restore the nco frequency to the same values as before the sync source
# was changed, to ensure the device transmission/acquisition continues at
# the requested frequency.
self.rfdc.rfdc_restore_nco_freq()
# Do the same for the calibration freeze state
self.rfdc.rfdc_restore_cal_freeze()
# Call synchronize to trigger the tile latency determination again. This is necessary
# to prevent us from bad EVM after changing the sync source.
self.synchronize(None, False)
except RuntimeError as ex:
err = f"Setting clock_source={clock_source},time_source={time_source} " \
f"failed, falling back to {self._safe_sync_source}. Error: " \
f"{ex}"
self.log.error(err)
if args.get('__noretry__', False):
self.log.error("Giving up.")
else:
self.set_sync_source({**self._safe_sync_source, '__noretry__': True})
raise
#######################################################################
# Timekeeper API (this is a public MPM API)
#######################################################################
def get_clocks(self):
"""
Gets the RFNoC-related clocks present in the FPGA design
TODO: The plan is to have the FPGA provide a list of clocks. This would
also allow user-defined clocks to show up in this list.
For now, we hardcode the list of clocks to our best knowledge. The main
issue with that is, we have to manually match the frequency of the clocks
with what the HDL implementation is using.
The following clock indices are fixed, and need to be changed in the
BSP YAML files (x410_bsp.yml and x440_bsp.yml) if they are changed here,
and vice versa. Note that clocks 1-3 are also hard-coded in image_builder.py.
1 - ctrl clk
2 - chdr clk
3 - ce clk
4,5 - radio clks
6,7 - radio clks 2x
Note: Even if we were able to read back the clock frequency for some
clocks from the FPGA, this wouldn't trivially be possible with the radio
clocks, as we spend a lot of work in this file to generate them in the
first place. Therefore, we populate their rates in this function manually.
"""
return [
### These clocks should be read back from the FPGA!
{ # Alias for clock 1: Control clock
'name': 'ctrl_clk',
'freq': str(40e6),
},
{
'name': '1',
'freq': str(40e6),
},
{ # Alias for clock 2: Bus/CHDR clock
'name': 'bus_clk',
'freq': str(200e6),
},
{ # Alias for clock 2: Bus/CHDR clock
'name': 'chdr_clk',
'freq': str(200e6),
},
{
'name': '2',
'freq': str(200e6),
},
{ # CE clock
'name': '3',
'freq': str(266.66667e6),
},
### These clocks will likely be always defined here
{ # Alias for clock 4: radio clock 0
'name': 'radio_clk',
'freq': str(self.get_master_clock_rate(0)),
'mutable': 'true'
},
{
'name': '4',
'freq': str(self.get_master_clock_rate(0)),
'mutable': 'true'
},
# We don't need an alias for clock 4, because it is not referenced
# in the block registry.
{
'name': '5',
'freq': str(self.get_master_clock_rate(1)),
'mutable': 'true'
},
### Now we should be asking the FPGA for any other clocks and their
### attributes.
]
###########################################################################
# Ref-Clk tuning word APIs.
#
# These calls will be available as MPM calls. They are a discoverable
# feature.
###########################################################################
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")
###########################################################################
# Synchronization API
###########################################################################
def synchronize(self, sync_args, finalize):
"""
See PeriphManagerBase.synchronize() for the full documentation.
"""
def determine_sync_settings():
"""
This is the algorithm that is run when finalize is False: Determine
the values.
"""
self.log.trace("Determining sync settings...")
sync_args_result = {}
rates = [self.rfdc.get_converter_rate(db_idx) for db_idx in range(2)]
if rates[0] != rates[1] or \
(len(self._master_clock_rates) > 1 and
self._master_clock_rates[0] != self._master_clock_rates[1]):
self.log.info("Multiple master clock rates detected: Skipping Multi-Tile "
"Synchronization, channels may not be fully synchronized!")
return sync_args_result
else:
db_keys = [('all', 'adc_latency', 'dac_latency')]
for db_key, adc_lat_key, dac_lat_key in db_keys:
adc_latencies, dac_latencies = \
self.rfdc.determine_tile_latencies(db_key)
self.log.debug(
f"Determined ADC tile latencies: "
f"{','.join(f'{k}={v}' for k,v in adc_latencies.items())}")
self.log.debug(
f"Determined DAC tile latencies: "
f"{','.join(f'{k}={v}' for k,v in dac_latencies.items())}")
# We need to pick a scalar latency value from all the latencies.
# For now, we just pick the first.
adc_latency = adc_latencies[0]
dac_latency = dac_latencies[0]
sync_args_result[adc_lat_key] = str(adc_latency)
sync_args_result[dac_lat_key] = str(dac_latency)
return sync_args_result
def finalize_sync_settings(sync_args):
"""
This is the algorithm that is run when finalize is True: Apply the
latency values.
"""
self.log.trace("Finalizing synchronization settings.")
if 'adc_latency' in sync_args and \
'dac_latency' in sync_args:
return self.rfdc.set_tile_latencies(
'all',
int(sync_args['adc_latency']),
int(sync_args['dac_latency']))
if len(sync_args) == 0:
self.log.debug("Empty sync args, not finalizing sync settings")
return True
self.log.error("Invalid sync args provided!")
raise RuntimeError("Invalid sync args provided!")
# Go, go, go!
if not finalize:
return determine_sync_settings()
if not finalize_sync_settings(sync_args):
self.log.error("Failed to apply synchronization settings!")
# align the ADC and DAC chains to have a static latency after synchronization
self.rfdc.reset_gearboxes()
return {}
def aggregate_sync_data(self, collated_sync_data):
"""
See PeriphManagerBase.aggregate_sync_data() for the full documentation.
"""
self.log.trace("Aggregating sync data...")
return {} if not collated_sync_data else collated_sync_data[0]
###########################################################################
# Helpers
###########################################################################
@no_rpc
def pop_host_tasks(self, task):
"""
Returns if a task should be executed by the host. Self-Cal is a task
to begin with.
"""
return self.tasks.pop(task,[])
###########################################################################
# 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.clk_ctrl.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
"""
previous_priref_rate = self._rpll_priref_rate
self._rpll_priref_rate = 25e6
# 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
self._config_rpll(self.X400_DEFAULT_MGT_CLOCK_RATE,
self.X400_DEFAULT_INT_CLOCK_FREQ,
RpllRefSel.PRIMARY)
# remove clkaux_nsync_clk as a valid reference source for later calls
# to _config_rpll(), it is only valid in this configuration
self._rpll_priref_rate = previous_priref_rate
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.clk_ctrl.get_fpga_aux_ref_freq()
|