# # Copyright 2018 Ettus Research, a National Instruments Company # # SPDX-License-Identifier: GPL-3.0-or-later # """ Helper class to initialize a Rhodium daughterboard """ from __future__ import print_function import time from usrp_mpm.sys_utils.uio import open_uio from usrp_mpm.dboard_manager.lmk_rh import LMK04828Rh from usrp_mpm.dboard_manager.rh_periphs import DboardClockControl from usrp_mpm.cores import ClockSynchronizer from usrp_mpm.cores import nijesdcore from usrp_mpm.cores.eyescan import EyeScanTool from usrp_mpm.dboard_manager.gain_rh import GainTableRh class RhodiumInitManager(object): """ Helper class: Holds all the logic to initialize an N320/N321 (Rhodium) daughterboard. """ # The Phase DAC is set at midscale, having its flatness validate +/- 1023 codes # from this initial value. INIT_PHASE_DAC_WORD = 32768 PHASE_DAC_SPI_ADDR = 0x3 # External PPS pipeline delay from the PPS captured at the FPGA to TDC input, # in reference clock ticks EXT_PPS_DELAY = 5 # Variable PPS delay before the RP/SP pulsers begin. Fixed value for the N3xx devices. N3XX_INT_PPS_DELAY = 4 # JESD core default configuration. JESD_DEFAULT_ARGS = {"lmfc_divider" : 12, "rx_sysref_delay": 5, "tx_sysref_delay": 11, "tx_driver_swing": 0b1101, "tx_precursor" : 0b00100, "tx_postcursor" : 0b00100} # After testing the roundtrip latency (i.e. FPGA -> TX -> RX -> FPGA), # it was found that a different value of RX SYSREF delay is required # for sampling_clock_rate = 400 MSPS to achieve latency consistency. RX_SYSREF_DLY_DIC = {400e6: 6, 491.52e6: 5, 500e6: 5} def __init__(self, rh_class, spi_ifaces): self.rh_class = rh_class self._spi_ifaces = spi_ifaces self.adc = rh_class.adc self.dac = rh_class.dac self.slot_idx = rh_class.slot_idx self.log = rh_class.log.getChild('init') def _init_lmk(self, lmk_spi, ref_clk_freq, sampling_clock_rate, pdac_spi, init_phase_dac_word, phase_dac_spi_addr): """ Sets the phase DAC to initial value, and then brings up the LMK according to the selected ref clock frequency. Will throw if something fails. """ self.log.trace("Initializing Phase DAC to d{}.".format( init_phase_dac_word )) pdac_spi.poke16(phase_dac_spi_addr, init_phase_dac_word) return LMK04828Rh(self.slot_idx, lmk_spi, ref_clk_freq, sampling_clock_rate, self.log) def _sync_db_clock(self, dboard_ctrl_regs, ref_clk_freq, master_clock_rate, args): " Synchronizes the DB clock to the common reference " reg_offset = 0x200 ext_pps_delay = self.EXT_PPS_DELAY if args.get('time_source', self.rh_class.default_time_source) == 'sfp0': reg_offset = 0x400 ref_clk_freq = 62.5e6 ext_pps_delay = 1 # only 1 flop between the WR core output and the TDC input synchronizer = ClockSynchronizer( dboard_ctrl_regs, self.rh_class.lmk, self._spi_ifaces['phase_dac'], reg_offset, master_clock_rate, ref_clk_freq, 1.116E-12, # fine phase shift. TODO don't hardcode. This should live in the EEPROM self.INIT_PHASE_DAC_WORD, self.PHASE_DAC_SPI_ADDR, ext_pps_delay, self.N3XX_INT_PPS_DELAY, self.slot_idx) # The radio clock traces on the motherboard are 69 ps longer for Daughterboard B # than Daughterboard A. We want both of these clocks to align at the converters # on each board, so adjust the target value for DB B. This is an N3xx series # peculiarity and will not apply to other motherboards. trace_delay_offset = {0: 0.0e-0, 1: 69.0e-12}[self.slot_idx] offset_error = abs(synchronizer.run( num_meas=[512, 128], target_offset=trace_delay_offset)) if offset_error > 100e-12: self.log.error("Residual clock synchronization offset error is invalid! " "Expected: <100 ps Actual: {:.1f} ps!".format(offset_error*1e12)) raise RuntimeError("Clock synchronization offset error is greater than expected.") else: self.log.debug("Residual synchronization error: {:.1f} ps.".format( offset_error*1e12 )) synchronizer = None self.log.debug("Sample Clock Synchronization Complete!") def set_jesd_rate(self, jesdcore, new_rate, current_jesd_rate, force=False): """ Make the QPLL and GTX changes required to change the JESD204B core rate. """ # The core is directly compiled for 500 MHz sample rate, which # corresponds to a lane rate of 5.0 Gbps. The same QPLL and GTX settings apply # for the 491.52 MHz sample rate. # # The lower non-LTE rate, 400 MHz, requires changes to the default configuration # of the MGT components. This function performs the required changes in the # following order (as recommended by UG476). # # 1) Modify any QPLL settings. # 2) Perform the QPLL reset routine by pulsing reset then waiting for lock. # 3) Modify any GTX settings. # 4) Perform the GTX reset routine by pulsing reset and waiting for reset done. assert new_rate in (4000e6, 4915.2e6, 5000e6) # On first run, we have no idea how the FPGA is configured... so let's force an # update to our rate. force = force or (current_jesd_rate is None) skip_drp = False if not force: # Current New Skip? skip_drp = {4000.0e6 : {4000.0e6: True , 4915.2e6: False, 5000.0e6: False}, 4915.2e6 : {4000.0e6: False, 4915.2e6: True , 5000.0e6: True }, 5000.0e6 : {4000.0e6: False, 4915.2e6: True , 5000.0e6: True }}[current_jesd_rate][new_rate] if skip_drp: self.log.trace("Current lane rate is compatible with the new rate. Skipping " "reconfiguration.") # These are the only registers in the QPLL and GTX that change based on the # selected line rate. The MGT wizard IP was generated for each of the rates and # reference clock frequencies and then diffed to create this table. QPLL_CFG = {4000.0e6: 0x6801C1, 4915.2e6: 0x680181, 5000.0e6: 0x0680181}[new_rate] MGT_RX_CLK25_DIV = {4000.0e6: 8, 4915.2e6: 10, 5000.0e6: 10}[new_rate] MGT_TX_CLK25_DIV = {4000.0e6: 8, 4915.2e6: 10, 5000.0e6: 10}[new_rate] # 1-2) Do the QPLL first if not skip_drp: self.log.trace("Changing QPLL settings to support {} Gbps".format(new_rate/1e9)) jesdcore.set_drp_target('qpll', 0) # QPLL_CONFIG is spread across two regs: 0x32 (dedicated) and 0x33 (shared) reg_x32 = QPLL_CFG & 0xFFFF # [16:0] -> [16:0] reg_x33 = jesdcore.drp_access(rd=True, addr=0x33) reg_x33 = (reg_x33 & 0xF800) | ((QPLL_CFG >> 16) & 0x7FF) # [26:16] -> [11:0] jesdcore.drp_access(rd=False, addr=0x32, wr_data=reg_x32) jesdcore.drp_access(rd=False, addr=0x33, wr_data=reg_x33) # Run the QPLL reset sequence and prep the MGTs for modification. jesdcore.init() # 3-4) And the 4 MGTs second if not skip_drp: self.log.trace("Changing MGT settings to support {} Gbps" .format(new_rate/1e9)) for lane in range(4): jesdcore.set_drp_target('mgt', lane) # MGT_RX_CLK25_DIV is embedded with others in 0x11. The # encoding for the DRP register value is one less than the # desired value. reg_x11 = jesdcore.drp_access(rd=True, addr=0x11) reg_x11 = (reg_x11 & 0xF83F) | \ ((MGT_RX_CLK25_DIV-1 & 0x1F) << 6) # [10:6] jesdcore.drp_access(rd=False, addr=0x11, wr_data=reg_x11) # MGT_TX_CLK25_DIV is embedded with others in 0x6A. The # encoding for the DRP register value is one less than the # desired value. reg_x6a = jesdcore.drp_access(rd=True, addr=0x6A) reg_x6a = (reg_x6a & 0xFFE0) | (MGT_TX_CLK25_DIV-1 & 0x1F) # [4:0] jesdcore.drp_access(rd=False, addr=0x6A, wr_data=reg_x6a) self.log.trace("GTX settings changed to support {} Gbps" .format(new_rate/1e9)) jesdcore.disable_drp_target() self.log.trace("JESD204b Lane Rate set to {} Gbps!" .format(new_rate/1e9)) return new_rate def init_jesd(self, jesdcore, sampling_clock_rate): """ Bringup the JESD links between the ADC, DAC, and the FPGA. All clocks must be set up and stable before starting this routine. """ jesdcore.check_core() # JESD Lane Rate only depends on the sampling_clock_rate selection, since all # other link parameters (LMFS,N) remain constant. L = 4 M = 2 F = 1 S = 1 N = 16 new_rate = sampling_clock_rate * M * N * (10.0/8) / L / S self.log.trace("Calculated JESD204B lane rate is {} Gbps".format(new_rate/1e9)) self.rh_class.current_jesd_rate = \ self.set_jesd_rate(jesdcore, new_rate, self.rh_class.current_jesd_rate) self.log.trace("Setting up JESD204B TX blocks.") jesdcore.init_framer() # Initialize FPGA's framer. self.adc.init_framer() # Initialize ADC's framer. self.log.trace("Enabling SYSREF capture blocks.") self.dac.enable_sysref_capture(True) # Enable DAC's SYSREF capture. self.adc.enable_sysref_capture(True) # Enable ADC's SYSREF capture. jesdcore.enable_lmfc(True) # Enable FPGA's SYSREF capture. self.log.trace("Setting up JESD204B DAC RX block.") self.dac.init_deframer() # Initialize DAC's deframer. self.log.trace("Sending SYSREF to all devices.") jesdcore.send_sysref_pulse() # Send SYSREF to all devices. self.log.trace("Setting up JESD204B FPGA RX block.") jesdcore.init_deframer() # Initialize FPGA's deframer. self.log.trace("Disabling SYSREF capture blocks.") self.dac.enable_sysref_capture(False) # Disable DAC's SYSREF capture. self.adc.enable_sysref_capture(False) # Disable ADC's SYSREF capture. jesdcore.enable_lmfc(False) # Disable FPGA's SYSREF capture. time.sleep(0.100) # Allow time for CGS/ILA. self.log.trace("Verifying JESD204B link status.") error_flag = False if not jesdcore.get_framer_status(): self.log.error("JESD204b FPGA Core Framer is not synced!") error_flag = True if not self.dac.check_deframer_status(): self.log.error("DAC JESD204B Deframer is not synced!") error_flag = True if not self.adc.check_framer_status(): self.log.error("ADC JESD204B Framer is not synced!") error_flag = True if not jesdcore.get_deframer_status(): self.log.error("JESD204B FPGA Core Deframer is not synced!") error_flag = True if error_flag: raise RuntimeError('JESD204B Link Initialization Failed. See MPM logs for details.') self.log.info("JESD204B Link Initialization & Training Complete") def init(self, args): """ Run the full initialization sequence. This will bring everything up from scratch: The LMK, JESD cores, the AD9695, the DAC37J82, and anything else that is clocking-related. Depending on the settings, this can take a fair amount of time. """ # Input validation on RX margin tests (@ FPGA and DAC) # By accepting the rx_eyescan/tx_prbs argument being str or bool, one may # request an eyescan measurement to be performed from either the USRP's # shell (i.e. using --default-args) or from the host's MPM shell. perform_rx_eyescan = False if 'rx_eyescan' in args: perform_rx_eyescan = (args['rx_eyescan'] == 'True') or (args['rx_eyescan'] == True) if perform_rx_eyescan: self.log.trace("Adding RX eye scan PMA enable to JESD args.") self.JESD_DEFAULT_ARGS["enable_rx_eyescan"] = True perform_tx_prbs = False if 'tx_prbs' in args: perform_tx_prbs = (args['tx_prbs'] == 'True') or (args['tx_prbs'] == True) # Latency across the JESD204B TX/RX links should remain constant and # deterministic across the supported sampling_clock_rate values. # After testing the roundtrip latency (i.e. FPGA -> TX -> RX -> FPGA), # it was found that a different set of SYSREF delay values are required # for sampling_clock_rate = 400 MSPS to achieve latency consistency. self.JESD_DEFAULT_ARGS['rx_sysref_delay'] = \ self.RX_SYSREF_DLY_DIC[self.rh_class.sampling_clock_rate] # Bringup Sequence. # 1. Prerequisites (include opening mmaps) # 2. Initialize LMK and bringup clocks. # 3. Synchronize DB Clocks. # 4. Initialize FPGA JESD IP. # 5. DAC Configuration. # 6. ADC Configuration. # 7. JESD204B Initialization. # 8. CPLD Gain Tables Initialization. # 1. Prerequisites # Open FPGA IP (Clock control and JESD core). self.log.trace("Creating gain table object...") self.gain_table_loader = GainTableRh( self._spi_ifaces['cpld'], self._spi_ifaces['cpld_gain_loader'], self.log) with open_uio( label="dboard-regs-{}".format(self.rh_class.slot_idx), read_only=False ) as radio_regs: self.log.trace("Creating dboard clock control object") db_clk_control = DboardClockControl(radio_regs, self.log) self.log.trace("Creating jesdcore object") jesdcore = nijesdcore.NIJESDCore(radio_regs, self.rh_class.slot_idx, **self.JESD_DEFAULT_ARGS) # 2. Initialize LMK and bringup clocks. # Disable FPGA MMCM's outputs, and assert its reset. db_clk_control.reset_mmcm() # Always place the JESD204b cores in reset before modifying the clocks, # otherwise high power or erroneous conditions could exist in the FPGA! jesdcore.reset() # Configure and bringup the LMK's clocks. self.log.trace("Initializing LMK...") self.rh_class.lmk = self._init_lmk( self._spi_ifaces['lmk'], self.rh_class.ref_clock_freq, self.rh_class.sampling_clock_rate, self._spi_ifaces['phase_dac'], self.INIT_PHASE_DAC_WORD, self.PHASE_DAC_SPI_ADDR ) self.log.trace("LMK Initialized!") # Deassert FPGA's MMCM reset, poll for lock, and enable outputs. db_clk_control.enable_mmcm() # 3. Synchronize DB Clocks. # The clock synchronzation driver receives the master_clock_rate, which for # Rhodium is half the sampling_clock_rate. self._sync_db_clock( radio_regs, self.rh_class.ref_clock_freq, self.rh_class.sampling_clock_rate / 2, args) # 4. DAC Configuration. self.dac.config() # 5. ADC Configuration. self.adc.config() # 6-7. JESD204B Initialization. self.init_jesd(jesdcore, self.rh_class.sampling_clock_rate) # [Optional] Perform RX eyescan. if perform_rx_eyescan: self.log.info("Performing RX eye scan on ADC to FPGA link...") self._rx_eyescan(jesdcore, args) # [Optional] Perform TX PRBS test. if perform_tx_prbs: self.log.info("Performing TX PRBS-31 test on FPGA to DAC link...") self._tx_prbs_test(jesdcore, args) # Direct the garbage collector by removing our references jesdcore = None db_clk_control = None # 8. CPLD Gain Tables Initialization. self.gain_table_loader.init() return True ########################################################################## # JESD204B RX margin testing ########################################################################## def _rx_eyescan(self, jesdcore, args): """ This function creates an eyescan object to perform this measurement with the given configuration and lanes. Parameters: prescale -> Controls the prescaling of the sample count to keep both sample count and error count in reasonable precision. Valid values: from 0 to 31. """ # The following constants must be defined according to GTs configuration # for each project. For further details, refer to the eyescan.py file. # For Rhodium, these parameters are based on the JESD core. rxout_div = 2 rx_int_datawidth = 20 eq_mode = 'LPM' # The following variables define the GTs to be scanned and the range of the # measurement. prescale = 0 scan_lanes = [0, 1, 2, 3] hor_range = {'start':-32 , 'stop':32 , 'step': 2} ver_range = {'start':-127, 'stop':127, 'step': 2} # Set default configuration values for Rhodium when the user is not intentionally # changing the constants/variables default values. for key in ('rxout_div', 'rx_int_datawidth', 'eq_mode', 'prescale', 'scan_lanes', 'hor_range', 'ver_range'): if key not in args: self.log.trace("Setting Rh default value for {0}... val: {1}" .format(key, locals()[key])) args[key] = locals()[key] # # Create an eyescan object. assert jesdcore is not None eyescan_tool = EyeScanTool(jesdcore, self.slot_idx, **args) # Put the ADC in pseudorandom test mode. adc_regs = self._spi_ifaces['adc'] # test_val = adc_regs.peek8(0x0550) # adc_regs.poke8(0x0550, 0x05) test_val = adc_regs.peek8(0x0573) adc_regs.poke8(0x0573, 0x13) # Perform eye scan on given lanes and range. file_name = eyescan_tool.eyescan_full_scan(args['scan_lanes'], args['hor_range'], args['ver_range']) # Do some housekeeping... # adc_regs.poke8(0x0550, test_val) # Enable normal operation. adc_regs.poke8(0x0573, test_val) # Enable normal operation. adc_regs.poke8(0x0000, 0x81) # Reset. eyescan_tool = None return file_name def _tx_prbs_test(self, jesdcore, args): """ This function allows to test the PRBS-31 pattern at the DAC. """ def _test_lanes(**tx_settings): """ This methods enables, monitors, and disables the PRBS-31 test. """ results = [] jesdcore.adjust_tx_phy(**tx_phy_settings) self.log.info("Testing TX PHY settings: tx_driver_swing=0b{0:04b}" " tx_precursor=0b{1:05b}" " tx_postcursor=0b{2:05b}" .format(tx_phy_settings["tx_driver_swing"], tx_phy_settings["tx_precursor"], tx_phy_settings["tx_postcursor"])) # Enable the GTs TX pattern generator in PRBS-31 mode. jesdcore.set_pattern_gen(mode='PRBS-31') # Monitor each receive lane at DAC. for lane_num in range(0, 4): self.dac.test_mode(mode='PRBS-31', lane=lane_num) # Enable PRBS test mode. number_of_failures = 0 for _ in range(0, POLLS_PER_GT): time.sleep(WAIT_TIME_PER_POLL) alarm_pin_dac = self.rh_class.cpld.get_dac_alarm() if alarm_pin_dac: number_of_failures += 1 results.append(number_of_failures) if number_of_failures > 0: self.log.error("PRBS-31 test for DAC lane {0} failed {1}/{2}!" .format(lane_num, number_of_failures, POLLS_PER_GT)) else: self.log.info("PRBS-31 test for DAC lane {0} passed!" .format(lane_num)) self.dac.test_mode(mode='OFF', lane=lane_num) # Disable PRBS test mode. # Disable TX pattern generator at FPGA jesdcore.set_pattern_gen(mode='OFF') return results # WAIT_TIME_PER_POLL = 0.001 # in seconds. POLLS_PER_GT = 100 # Create the CSV file. f = open('tx_prbs_sweep.csv', 'w') f.write("Swing,Precursor,Postcursor,Polls,Failures 0,Failures 1,Failures 2,Failures 3\n") # Default TX PHY settings. tx_phy_settings = {"tx_driver_swing": 0b1111, # See UG476, TXDIFFCTRL "tx_precursor" : 0b00000, # See UG476, TXPRECURSOR "tx_postcursor" : 0b00000} # See UG476, TXPOSTCURSOR # Define sweep ranges. DEFAULT_SWING_RANGE = {'start': 0b0000, 'stop': 0b1111 + 0b1, 'step': 1} DEFAULT_CURSOR_RANGE = {'start': 0b00000, 'stop': 0b11111 + 0b1, 'step': 2} swing_range = args.get("swing_range", DEFAULT_SWING_RANGE) precursor_range = args.get("precursor_range", DEFAULT_CURSOR_RANGE) postcursor_range = args.get("postcursor_range", DEFAULT_CURSOR_RANGE) # Test the TX margin across multiple PHY settings. for swing in range(swing_range['start'], swing_range['stop'], swing_range['step']): tx_phy_settings["tx_driver_swing"] = swing for precursor in range(precursor_range['start'], precursor_range['stop'], precursor_range['step']): tx_phy_settings["tx_precursor"] = precursor for postcursor in range(postcursor_range['start'], postcursor_range['stop'], postcursor_range['step']): tx_phy_settings["tx_postcursor"] = postcursor results = _test_lanes(**tx_phy_settings) f.write("{},{},{},{},{},{},{},{}\n".format( tx_phy_settings["tx_driver_swing"], tx_phy_settings["tx_precursor"], tx_phy_settings["tx_postcursor"], POLLS_PER_GT, results[0], results[1], results[2], results[3])) # Housekeeping... f.close()