diff options
author | Mark Meserve <mark.meserve@ni.com> | 2018-10-24 15:24:21 -0500 |
---|---|---|
committer | Brent Stapleton <bstapleton@g.hmc.edu> | 2018-10-25 10:30:59 -0700 |
commit | d87ae61d0490339bfbd7de5d57e692e9e8961237 (patch) | |
tree | 3e11b3c5f3c0aab3624dadf78ddcbcb2af85b3b9 /mpm/python/usrp_mpm/dboard_manager/rh_init.py | |
parent | ec2977d8cbb233988b93f81deab7af99daec4165 (diff) | |
download | uhd-d87ae61d0490339bfbd7de5d57e692e9e8961237.tar.gz uhd-d87ae61d0490339bfbd7de5d57e692e9e8961237.tar.bz2 uhd-d87ae61d0490339bfbd7de5d57e692e9e8961237.zip |
rh: add support for rhodium devices
Co-authored-by: Humberto Jimenez <humberto.jimenez@ni.com>
Co-authored-by: Alex Williams <alex.williams@ni.com>
Co-authored-by: Derek Kozel <derek.kozel@ni.com>
Diffstat (limited to 'mpm/python/usrp_mpm/dboard_manager/rh_init.py')
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/rh_init.py | 481 |
1 files changed, 481 insertions, 0 deletions
diff --git a/mpm/python/usrp_mpm/dboard_manager/rh_init.py b/mpm/python/usrp_mpm/dboard_manager/rh_init.py new file mode 100644 index 000000000..b4d4dded6 --- /dev/null +++ b/mpm/python/usrp_mpm/dboard_manager/rh_init.py @@ -0,0 +1,481 @@ +# +# 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 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. + """ + # After manually probing the PLL1's reference and feedback signal from the LMK + # using multiple phase dac values close to its midpoint (2^11 = 2048), it was + # discovered that the PLL1's tightest phase lock is at 2024. + INIT_PHASE_DAC_WORD = 32768 # TODO: update this number for Rev. B + 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. + # TODO: define the actual values for rx_sysref_delay and tx_sysref_delay. + JESD_DEFAULT_ARGS = {"lmfc_divider" : 12, + "rx_sysref_delay": 8, + "tx_sysref_delay": 11, + "tx_driver_swing": 0b1101, + "tx_precursor" : 0b00100, + "tx_postcursor" : 0b00100} + + + 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) + + + # TODO: update phase shift value after testing phase DAC flatness with shields (Rev. B) + 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.1E-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("Clock synchronizer measured an offset of {:.1f} ps!".format( + offset_error*1e12 + )) + self.log.warning("RuntimeError is not being thrown for Rhodium Rev. A") + # raise RuntimeError("Clock synchronizer measured an offset of {:.1f} ps!".format( + # offset_error*1e12 + # )) + 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(True): # TODO: Remove the boolean argument! + 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) + + # 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 dboard clock control object") + db_clk_control = DboardClockControl(self.rh_class.radio_regs, self.log) + self.log.trace("Creating jesdcore object") + jesdcore = nijesdcore.NIJESDCore(self.rh_class.radio_regs, self.rh_class.slot_idx, **self.JESD_DEFAULT_ARGS) + + self.log.trace("Creating gain table object...") + self.gain_table_loader = GainTableRh( + self._spi_ifaces['cpld'], + self._spi_ifaces['cpld_gain_loader'], + self.log) + + # 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( + self.rh_class.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) + jesdcore = None # We are done using the jesdcore at this point. + + # 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() |