diff options
Diffstat (limited to 'mpm')
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt | 8 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/__init__.py | 1 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/adc_rh.py | 242 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/dac_rh.py | 342 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/gain_rh.py | 93 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/gaintables_rh.py | 271 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/lmk_rh.py | 316 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/rh_init.py | 481 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/rh_periphs.py | 193 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/rhodium.py | 573 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/n3xx.py | 6 |
11 files changed, 2525 insertions, 1 deletions
diff --git a/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt b/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt index 64217be07..0b34e4d70 100644 --- a/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt +++ b/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt @@ -11,8 +11,16 @@ SET(USRP_MPM_FILES ${USRP_MPM_FILES}) SET(USRP_MPM_DBMGR_FILES ${CMAKE_CURRENT_SOURCE_DIR}/__init__.py ${CMAKE_CURRENT_SOURCE_DIR}/base.py + ${CMAKE_CURRENT_SOURCE_DIR}/rhodium.py + ${CMAKE_CURRENT_SOURCE_DIR}/rh_init.py + ${CMAKE_CURRENT_SOURCE_DIR}/rh_periphs.py + ${CMAKE_CURRENT_SOURCE_DIR}/lmk_rh.py + ${CMAKE_CURRENT_SOURCE_DIR}/adc_rh.py + ${CMAKE_CURRENT_SOURCE_DIR}/dac_rh.py ${CMAKE_CURRENT_SOURCE_DIR}/eiscat.py ${CMAKE_CURRENT_SOURCE_DIR}/neon.py + ${CMAKE_CURRENT_SOURCE_DIR}/gain_rh.py + ${CMAKE_CURRENT_SOURCE_DIR}/gaintables_rh.py ${CMAKE_CURRENT_SOURCE_DIR}/lmk_eiscat.py ${CMAKE_CURRENT_SOURCE_DIR}/lmk_mg.py ${CMAKE_CURRENT_SOURCE_DIR}/magnesium.py diff --git a/mpm/python/usrp_mpm/dboard_manager/__init__.py b/mpm/python/usrp_mpm/dboard_manager/__init__.py index 8f6e5da96..70e7881db 100644 --- a/mpm/python/usrp_mpm/dboard_manager/__init__.py +++ b/mpm/python/usrp_mpm/dboard_manager/__init__.py @@ -8,6 +8,7 @@ dboards module __init__.py """ from .base import DboardManagerBase from .magnesium import Magnesium +from .rhodium import Rhodium from .neon import Neon from .eiscat import EISCAT from .test import test diff --git a/mpm/python/usrp_mpm/dboard_manager/adc_rh.py b/mpm/python/usrp_mpm/dboard_manager/adc_rh.py new file mode 100644 index 000000000..2befa011f --- /dev/null +++ b/mpm/python/usrp_mpm/dboard_manager/adc_rh.py @@ -0,0 +1,242 @@ +# +# Copyright 2018 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +AD9695 driver for use with Rhodium +""" + +import time +from builtins import object +from ..mpmlog import get_logger + +class AD9695Rh(object): + """ + This class provides an interface to configure the AD9695 IC through SPI. + """ + + ADC_CHIP_ID = 0xDE + + CHIP_CONFIGURATION_REG = 0x0002 + CHIP_ID_LSB_REG = 0x0004 + SCRATCH_PAD_REG = 0x000A + + def __init__(self, slot_idx, regs_iface, parent_log=None): + self.log = parent_log.getChild("AD9695") if parent_log is not None \ + else get_logger("AD9695-{}".format(slot_idx)) + self.regs = regs_iface + assert hasattr(self.regs, 'peek8') + assert hasattr(self.regs, 'poke8') + + def _verify_chip_id(): + chip_id = self.regs.peek8(self.CHIP_ID_LSB_REG) + self.log.trace("ADC Chip ID: 0x{:X}".format(chip_id)) + if chip_id != self.ADC_CHIP_ID: + self.log.error("Wrong Chip ID 0x{:X}".format(chip_id)) + return False + return True + + if not _verify_chip_id(): + raise RuntimeError("Unable to locate AD9695") + + + def assert_scratch(self, scratch_val=0xAD): + """ + Method that validates the scratch register by poking and peeking. + """ + self.regs.poke8(self.SCRATCH_PAD_REG, scratch_val) + self.log.trace("Scratch write value: 0x{:X}".format(scratch_val)) + scratch_rb = self.regs.peek8(self.SCRATCH_PAD_REG) & 0xFF + self.log.trace("Scratch readback: 0x{:X}".format(scratch_rb)) + if scratch_rb != scratch_val: + raise RuntimeError("Wrong ADC scratch readback: 0x{:X}".format(scratch_rb)) + + def pokes8(self, addr_vals): + """ + Apply a series of pokes. + pokes8((0,1),(0,2)) is the same as calling poke8(0,1), poke8(0,2). + """ + for addr, val in addr_vals: + self.regs.poke8(addr, val) + + + def power_down_channel(self, power_down=False): + """ + This method either powers up/down the channel according to register 0x0002 [1:0]: + power_down = True -> Power-down mode. + Digital datapath clocks disabled; digital datapath held + in reset; JESD204B interface disabled. + power_down = False -> Normal mode. + Channel powered up. + """ + power_mode = 0b11 if power_down else 0b00 + self.regs.poke8(self.CHIP_CONFIGURATION_REG, power_mode) + + + def init(self): + """ + Basic init that resets the ADC and verifies it. + """ + self.power_down_channel(False) # Power-up the channel. + self.log.trace("Reset ADC & Verify") + self.regs.poke8(0x0000, 0x81) # Soft-reset the ADC (self-clearing). + time.sleep(0.005) # We must allow 5 ms for the ADC's bootloader. + self.assert_scratch(0xAD) # Verify scratch register R/W access. + self.regs.poke8(0x0571, 0x15) # Powerdown the JESD204B serial transmit link. + self.log.trace("ADC's JESD204B link powered down.") + + + def config(self): + """ + Check the clock status, and write configuration values! + Before performing the above, the chip is soft-reset through the + serial interface. + """ + self.init() + + clock_status = self.regs.peek8(0x011B) & 0xFF + self.log.trace("Clock status readback: 0x{:X}".format(clock_status)) + if clock_status != 0x01: + self.log.error("Input clock not detected") + raise RuntimeError("Input clock not detected for ADC") + + self.log.trace("ADC Configuration.") + self.pokes8(( + (0x003F, 0x80), # Disable PDWN/STBY pin. + (0x0040, 0x00), # FD_A|B pins configured as Fast Detect outputs. + (0x0559, 0x11), # Configure tail bits as overrange bits (1:0) (Based on VST2). + (0x055A, 0x01), # Configure tail bits as overrange bits (2) (Based on VST2). + (0x058D, 0x17), # Set K = d23 (0x17) (Frames per multiframe). + (0x0550, 0x00), # Test mode pattern generation: OFF. + (0x0571, 0x15), # JESD Link mode: ILA enabled with K28.3 and K28.7; link powered down. + (0x058F, 0x0D), # CS = 0 (Control bits); N = 14 (ADC resolution). + (0x056E, 0x10), # JESD204B lane rate range: 3.375 Gbps to 6.75 Gbps. + (0x058B, 0x03), # Scrambling disabled; L = 4 (number of lanes). + (0x058C, 0x00), # F = 1 (0x0 + 1) (Number of octets per frame) + (0x058E, 0x01), # M = 2 (0x1) (Number of converters per link). + (0x05B2, 0x01), # SERDOUT0 mapped to lane 1. + (0x05B3, 0x00), # SERDOUT1 mapped to lane 0. + (0x05C0, 0x10), # SERDOUT0 voltage swing adjust (improves RX margin at FPGA). + (0x05C1, 0x10), # SERDOUT1 voltage swing adjust (improves RX margin at FPGA). + (0x05C2, 0x10), # SERDOUT2 voltage swing adjust (improves RX margin at FPGA). + (0x05C3, 0x10), # SERDOUT3 voltage swing adjust (improves RX margin at FPGA). + )) + self.log.trace("ADC register dump finished.") + + + def init_framer(self): + """ + Initialize the ADC's framer, and check the PLL for lock. + """ + def _check_pll_lock(): + pll_lock_status = self.regs.peek8(0x056F) + if (pll_lock_status & 0x88) != 0x80: + self.log.debug("PLL reporting unlocked... Status: 0x{:x}" + .format(pll_lock_status)) + return False + return True + + self.log.trace("Initializing framer...") + self.pokes8(( + (0x0571, 0x14), # Powerup the link before JESD204B initialization. + (0x1228, 0x4F), # Reset JESD204B start-up circuit + (0x1228, 0x0F), # JESD204B start-up circuit in normal operation. The value may be 0x00. + (0x1222, 0x00), # JESD204B PLL force normal operation + (0x1222, 0x04), # Reset JESD204B PLL calibration + (0x1222, 0x00), # JESD204B PLL normal operation + (0x1262, 0x08), # Clear loss of lock bit + (0x1262, 0x00), # Loss of lock bit normal operation + )) + + self.log.trace("Polling for PLL lock...") + locked = False + for _ in range(6): + time.sleep(0.001) + # Clear stickies possibly? + if _check_pll_lock(): + locked = True + self.log.info("ADC PLL Locked!") + break + if not locked: + raise RuntimeError("ADC PLL did not lock! Check the logs for details.") + + self.log.trace("ADC framer initialized.") + + def enable_sysref_capture(self, enabled=False): + """ + Enable the SYSREF capture block. + """ + sysref_ctl1 = 0x00 # Default value is disabled. + if enabled: + sysref_ctl1 = 0b1 << 2 # N-Shot SYSREF mode + + self.log.trace("%s ADC SYSREF capture..." % {True: 'Enabling', False: 'Disabling'}[enabled]) + self.pokes8(( + (0x0120, sysref_ctl1), # Capture low-to-high N-shot SYSREF transitions on CLK's RE + (0x0121, 0x00), # Capture the next SYSREF only. + )) + self.log.trace("ADC SYSREF capture %s." % {True: 'enabled', False: 'disabled'}[enabled]) + + + def check_framer_status(self): + """ + This function checks the status of the framer by checking SYSREF capture regs. + """ + SYSREF_MONITOR_MESSAGES = { + 0 : "Condition not defined!", + 1 : "Possible setup error. The smaller the setup, the smaller its margin.", + 2 : "No setup or hold error (best hold margin).", + 3 : "No setup or hold error (best setup and hold margin).", + 4 : "No setup or hold error (best setup margin).", + 5 : "Possible hold error. The largest the hold, the smaller its margin.", + 6 : "Possible setup or hold error." + } + + # This is based of Table 37 in the AD9695's datasheet. + def _decode_setup_hold(setup, hold): + status = 0 + if setup == 0x0: + if hold == 0x0: + status = 6 + elif hold == 0x8: + status = 4 + elif (hold >= 0x9) and (hold <= 0xF): + status = 5 + elif (setup <= 0x7) and (hold == 0x0): + status = 1 + elif (setup == 0x8) and ((hold >= 0x0) and (hold <= 0x8)): + status = 2 + elif hold == 0x8: + status = 3 + return status + + self.log.trace("Checking ADC's framer status.") + + # Read the SYSREF setup and hold monitor register. + sysref_setup_hold_monitor = self.regs.peek8(0x0128) + sysref_setup = sysref_setup_hold_monitor & 0x0F + sysref_hold = (sysref_setup_hold_monitor & 0xF0) >> 4 + sysref_monitor_status = _decode_setup_hold(sysref_setup, sysref_hold) + self.log.trace("SYSREF setup: 0x{:X}".format(sysref_setup)) + self.log.trace("SYSREF hold: 0x{:X}".format(sysref_hold)) + self.log.debug("SYSREF monitor: %s" % SYSREF_MONITOR_MESSAGES[sysref_monitor_status]) + + # Read the Clock divider phase when SYSREF is captured. + sysref_phase = self.regs.peek8(0x0129) & 0x0F + self.log.trace("SYSREF capture was %.2f cycle(s) delayed from clock.", (sysref_phase * 0.5)) + self.log.trace("Clk divider phase when SYSREF was captured: 0x{:X}".format(sysref_phase)) + + # Read the SYSREF counter. + sysref_count = self.regs.peek8(0x012A) & 0xFF + self.log.trace("%d SYSREF events were captured." % sysref_count) + + if sysref_count == 0x0: + self.log.error("A SYSREF event was not captured by the ADC.") + elif sysref_monitor_status == 0: + self.log.trace("The SYSREF setup & hold monitor status is not defined.") + elif sysref_monitor_status in (1, 5, 6): + self.log.warning("SYSREF monitor: %s" % SYSREF_MONITOR_MESSAGES[sysref_monitor_status]) + elif sysref_phase > 0x0: + self.log.trace("SYSREF capture was %.2f cycle(s) delayed from clock." % (sysref_phase * 0.5)) + return sysref_count >= 0x01 diff --git a/mpm/python/usrp_mpm/dboard_manager/dac_rh.py b/mpm/python/usrp_mpm/dboard_manager/dac_rh.py new file mode 100644 index 000000000..7fd32e8de --- /dev/null +++ b/mpm/python/usrp_mpm/dboard_manager/dac_rh.py @@ -0,0 +1,342 @@ +# +# Copyright 2018 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +DAC37J82 driver for use with Rhodium +""" + +import time +from builtins import object +from ..mpmlog import get_logger + +class DAC37J82Rh(object): + """ + This class provides an interface to configure the DAC37J82 IC through SPI. + """ + + DAC_VENDOR_ID = 0b01 + DAC_VERSION_ID = 0b010 # Version used in Rhodium Rev. A + + def __init__(self, slot_idx, regs_iface, parent_log=None): + self.log = parent_log.getChild("DAC37J82") if parent_log is not None \ + else get_logger("DAC37J82-{}".format(slot_idx)) + self.slot_idx = slot_idx + self.regs = regs_iface + assert hasattr(self.regs, 'peek16') + assert hasattr(self.regs, 'poke16') + + def _verify_chip_id(): + chip_id = self.regs.peek16(0x7F) & 0x001F + self.log.trace("DAC Vendor & Version ID: 0x{:X}".format(chip_id)) + if chip_id != ((self.DAC_VENDOR_ID << 3) | self.DAC_VERSION_ID): + self.log.error("Wrong Vendor & Version 0x{:X}".format(chip_id)) + return False + return True + + self.reset() + + if not _verify_chip_id(): + raise RuntimeError("Unable to locate DAC37J82") + + # Define variable configuration per slot. + # The JESD lanes going to the DAC pins are swapped differently: + # DBA: 0 -> 0 / 1 -> 1 / 2 -> 2 / 3 -> 3 + # DBB: 0 -> 0 / 1 -> 1 / 2 -> 3 / 3 -> 2 + # Therefore, depending on the DB that is being configured, we need + # to change the JESD lanes internal routing in the DAC to compensate + # for the board traces swapping. + self.lanes_ids_1 = {0: 0x0044, 1: 0x0046}[self.slot_idx] # config70 + self.lanes_ids_2 = {0: 0x190A, 1: 0x110A}[self.slot_idx] # config71 + self.octetpath_sel = {0: 0x0123, 1: 0x0132}[self.slot_idx] # config95 + + self.init() + + + def tx_enable(self, enable=False): + """ + Enable/disable the analog TX output. + """ + enable_bit = 0b1 if enable else 0b0 + prev_val = self.regs.peek16(0x03) + self.regs.poke16(0x03, prev_val | enable_bit) + + + def pokes16(self, addr_vals): + """ + Apply a series of pokes. + pokes16((0,1),(0,2)) is the same as calling poke16(0,1), poke16(0,2). + """ + for addr, val in addr_vals: + self.regs.poke16(addr, val) + + + def init(self): + """ + Basic init that disables the analog output. + """ + self.tx_enable(False) # Set TXENABLE low at the DAC + self.log.trace("DAC's Analog TX output is disabled.") + + def reset(self): + """ + Reset the DAC state + """ + self.regs.poke16(0x02, 0x2002) # Deassert the reset for the SIF registers + self.regs.poke16(0x02, 0x2003) # Assert the reset for the SIF registers + + def config(self): + """ + Check the clock status, and write configuration values! + """ + def _check_pll_lock(): + pll_ool_alarms = self.regs.peek16(0x6C) + if (pll_ool_alarms & 0x0008) != 0x0000: + self.log.warning("PLL reporting unlocked... Status: 0x{:x}" + .format(pll_ool_alarms)) + return False + return True + + self.log.trace("Reset DAC & Clear alarm bits") + self.reset() + self.regs.poke16(0x6C, 0x0000) # Clear alarm bits for PLLs + + self.log.trace("DAC Configuration.") + self.pokes16(( + (0x00, 0x0018), # config0: Interpolation 1x; ALARM enabled w/ pos. logic. + (0x01, 0x0003), # config1: Rewriting reserved default values. + (0x02, 0x0002), # config2: Data not zero when link not established; 2's comp. arrives at input. + (0x03, 0x9300), # config3: Coarse DAC = 10 (9+1); TXENABLE internal is kept low. + (0x04, 0x0000), # config4: Not masking any lane errors or FIFO flags. + (0x05, 0x0000), # config5: Not masking any SYSREF errors, PAPs, or PLL locks. + (0x06, 0x0000), # config6: Not masking any lane short test or loss of signal. + (0x08, 0x0000), # config8: DAC A offset correction set to zero (default). + (0x09, 0x0000), # config9: DAC B offset correction set to zero (default). + (0x0A, 0x0000), # config10: DAC C offset correction set to zero (default). + (0x0B, 0x0000), # config11: DAC D offset correction set to zero (default). + (0x0C, 0x0400), # config12: Default quadrature correction gain A (AB path). + (0x0D, 0x0400), # config13: Default coarse mixing options; default quadrature correction gain B (AB path). + (0x0E, 0x0400), # config14: Default quadrature correction gain A (CD path). + (0x0F, 0x0400), # config15: No output delays to the DACs; default quadrature correction gain A (CD path). + (0x10, 0x0000), # config16: Default QMC correction phase (AB path). + (0x11, 0x0000), # config17: Default QMC correction phase (AD path). + (0x12, 0x0000), # config18: Phase offset for NCO in DACAB path (default). + (0x13, 0x0000), # config19: Phase offset for NCO in DACAB path (default). + (0x14, 0x0000), # config20: Lower 16 bits of NCO Frequency adjust word for DACAB path (default). + (0x15, 0x0000), # config21: Middle 16 bits of NCO Frequency adjust word for DACAB path (default). + (0x16, 0x0000), # config22: Upper 16 bits of NCO Frequency adjust word for DACAB path (default). + (0x17, 0x0000), # config23: Lower 16 bits of NCO Frequency adjust word for DACCD path (default). + (0x18, 0x0000), # config24: Middle 16 bits of NCO Frequency adjust word for DACCD path (default). + (0x19, 0x0000), # config25: Upper 16 bits of NCO Frequency adjust word for DACCD path (default). + (0x1A, 0x0023), # config26: DAC PLL in sleep mode; DAC C & D in sleep mode. + (0x1B, 0x0000), # config27: Testing settings (default). + (0x1E, 0x2222), # config30: Sync source for the QMC offset and correction: SYSREF only (Based on VST2). + (0x1F, 0x2220), # config31: Sync source for mixers and NCO accums.: SYSREF only (Based on VST2). + (0x20, 0x0000), # config32: Sync source for the dithering, PA protection, and FIR filter blocks: none (Based on VST2). + (0x22, 0x1B1B), # config34: JESD and DAC routing paths (default). + (0x23, 0x01FF), # config35: SLEEP signal from pin allowed to reach all blocks, the pin is not even used. + (0x24, 0x0000), # config36: SYSREF syncs clock dividers: use no pulses yet. + (0x25, 0x2000), # config37: DACCLK divider to generate JESD clock: div2. (TI recommendation). + (0x26, 0x0000), # config38: Dithering disabled default). + (0x2D, 0x0000), # config45: Power amplifier protection settings (default). + (0x2E, 0xFFFF), # config46: Power amplifier protection threshold (default). + (0x2F, 0x0004), # config47: Default values. + (0x30, 0x0000), # config48: Constant value sent to DAC when sifdac_ena is asserted (default). + (0x31, 0x1000), # config49: DAC PLL in reset and disabled. DACCLK is 491.52 MHz since we bypass the PLL. + (0x32, 0x0000), # config50: DAC PLL's VCO feedback and prescaler divided (not used). + (0x33, 0x0000), # config51: DAC PLL VCO and CP settings (not used). + (0x34, 0x0000), # config52: SYNCB electrical configuration (default @ 1.2V CMV). + (0x3B, 0x0000), # config59: SerDes PLL's reference. Source: DACCLK / Divider: 1 (0 +1). + (0x3C, 0x1828), # config60: SerDes PLL Control: high loop BW; high range VCO; multiply factor x5. + (0x3D, 0x0088), # config61: Upper configuration info for SerDes receivers (TI recommendation). + (0x3E, 0x0128), # config62: Upper configuration for SerDes receivers:AC coupling; half rate; 20-bit width. + (0x3F, 0x0000), # config63: No SerDes lanes inversion (default). + (0x46, self.lanes_ids_1), # config70: JESD ID for lanes 0, 1, and 2. + (0x47, self.lanes_ids_2), # config71: JESD ID for lanes 3, 4, and 5. + (0x48, 0x31C3), # config72: JESD ID for lanes 6, and 7; JESD204B supported version and class (default). + (0x49, 0x0000), # config73: JESD lanes assignment to links (default 0). + (0x4A, 0x0F3E), # config74: Lanes 0-3 enabled; test seq. disabled; disable clocks to C/D paths. + (0x4B, 0x1700), # config75: RBD = 24 (23 + 1) (Release Buffer Delay); F = 1 (octets per frame). + (0x4C, 0x1703), # config76: K = 24 (23 + 1) (frames in multiframe); L = 4 (3 + 1) (number of lanes). + (0x4D, 0x0100), # config77: M = 2 (1+1) (number of converters); S = 1 (0+1) (number of samples per frame). + (0x4E, 0x0F4F), # config78: HD = 1 (High Density mode enabled, samples split across lanes). + (0x4F, 0x1CC1), # config79: Match /R/ char; ILA is supported at TX. + (0x50, 0x0000), # config80: Lane config data (link0), not used by 37J82 (default). + (0x51, 0x00FF), # config81: Erros that cause a SYNC request (link0): all selected (default). + (0x52, 0x00FF), # config82: Errors that are counted in err_c (link0): all selected (default). + (0x53, 0x0000), # config83: Lane config data (link1), not used by 37J82 (default). + (0x54, 0x0000), # config84: Erros that cause a SYNC request (link1): none selected. + (0x55, 0x0000), # config85: Errors that are counted in err_c (link1): none selected. + (0x56, 0x0000), # config86: Lane config data (link2), not used by 37J82 (default). + (0x57, 0x0000), # config87: Erros that cause a SYNC request (link2): none selected. + (0x58, 0x0000), # config88: Errors that are counted in err_c (link2): none selected. + (0x59, 0x0000), # config89: Lane config data (link3), not used by 37J82 (default). + (0x5A, 0x0000), # config90: Erros that cause a SYNC request (link2): none selected. + (0x5B, 0x0000), # config91: Errors that are counted in err_c (link3): none selected. + (0x5C, 0x0000), # config92: Links 3:1 don't use SYSREF pulses; link 0 uses no pulses yet. + (0x5E, 0x0000), # config94: Cheksum bits for ILA, not used in 37J82 (default). + (0x5F, self.octetpath_sel), # config95: Mapping SerDes lanes (0-3) to JESD lanes. + (0x60, 0x4567), # config96: Mapping SerDes lanes (4-7) to JESD lanes (default). + (0x61, 0x0001), # config97: Use only link 0 to trigger the SYNCB LVDS output. + (0x64, 0x0703), # config100: Write to lane 0 errors to clear them (based on VST2). + (0x65, 0x0703), # config101: Write to lane 1 errors to clear them (based on VST2). + (0x66, 0x0703), # config102: Write to lane 2 errors to clear them (based on VST2). + (0x67, 0x0703), # config103: Write to lane 3 errors to clear them (based on VST2). + (0x68, 0x0703), # config104: Write to lane 4 errors to clear them (based on VST2). + (0x69, 0x0703), # config105: Write to lane 5 errors to clear them (based on VST2). + (0x6A, 0x0703), # config106: Write to lane 6 errors to clear them (based on VST2). + (0x6B, 0x0703), # config107: Write to lane 7 errors to clear them (based on VST2). + (0x6C, 0x0000), # config108: Rewrite the PLLs alarm bits clearing register. + (0x6D, 0x0000), # config109: JESD short test alarms (default). + (0x6E, 0x0000), # config110: Delay fractional filter settings (default). + (0x6F, 0x0000), # config111: Delay fractional filter settings (default). + (0x70, 0x0000), # config112: Delay fractional filter settings (default). + (0x71, 0x0000), # config113: Delay fractional filter settings (default). + (0x72, 0x0000), # config114: Delay fractional filter settings (default). + (0x73, 0x0000), # config115: Delay fractional filter settings (default). + (0x74, 0x0000), # config116: Delay fractional filter settings (default). + (0x75, 0x0000), # config117: Delay fractional filter settings (default). + (0x76, 0x0000), # config118: Delay fractional filter settings (default). + (0x77, 0x0000), # config119: Delay fractional filter settings (default). + (0x78, 0x0000), # config120: Delay fractional filter settings (default). + (0x79, 0x0000), # config121: Delay fractional filter settings (default). + (0x7A, 0x0000), # config122: Delay fractional filter settings (default). + (0x7B, 0x0000), # config123: Delay fractional filter settings (default). + (0x7C, 0x0000), # config124: Delay fractional filter settings (default). + (0x7D, 0x0000), # config125: Delay fractional filter settings (default). + (0x02, 0x2002), # Deassert the reset for the SIF registers + )) + self.log.trace("DAC register dump finished.") + + self.log.trace("Polling for PLL lock...") + locked = False + for _ in range(6): + time.sleep(0.001) + # Clear stickies possibly? + self.regs.poke16(0x6C, 0x0000) # Clear alarm bits for PLLs + if _check_pll_lock(): + locked = True + self.log.info("DAC PLL Locked!") + break + if not locked: + raise RuntimeError("DAC PLL did not lock! Check the logs for details.") + + + def enable_sysref_capture(self, enabled=False): + """ + Enable the SYSREF capture block, and enable divider's reset. + """ + self.log.trace("%s DAC SYSREF capture...", + {True: 'Enabling', False: 'Disabling'}[enabled]) + cdrvser_sysref_mode = 0b001 if enabled else 0b000 + sysref_mode_link0 = 0b001 if enabled else 0b000 + self.regs.poke16(0x24, cdrvser_sysref_mode << 4) # Enable next SYSREF to reset the clock dividers. + self.regs.poke16(0x5C, sysref_mode_link0 << 0) # Enable next SYSREF pulse capture for link 0. + self.log.trace("DAC SYSREF capture %s." % {True: 'enabled', False: 'disabled'}[enabled]) + + + def init_deframer(self): + """ + Initialize the DAC's framer. + """ + self.log.trace("Initializing framer...") + self.pokes16(( + (0x4A, 0x0F3F), # config74: Deassert JESD204B block reset. + (0x4A, 0x0F21), # config74: Set JESD204B to exit init state. + )) + self.log.trace("DAC deframer initialized.") + + + def check_deframer_status(self): + """ + This function checks the status of the framer by checking alarms. + """ + ALARM_ERRORS_DESCRIPTION = { + 15 : "Multiframe alignment error", + 14 : "Frame alignment error", + 13 : "Link configuration error", + 12 : "Elastic buffer overflow", + 11 : "Elastic buffer match error", + 10 : "Code synchronization error", + 9 : "8b/10b not-in-table code error", + 8 : "8b/10b disparity error", + 3 : "FIFO write_error", + 2 : "FIFO write_full", + 1 : "FIFO read_error", + 0 : "FIFO read_empty" + } + + self.log.trace("Checking DAC's deframer status.") + # Clear lane alarms. + for addr in (0x64, 0x65, 0x66, 0x67): + self.regs.poke16(addr, 0x0000) + time.sleep(0.001) + + # Read lane's alarms + lanes_alarms_ary = [] + lane = 0 + for addr in (0x64, 0x65, 0x66, 0x67): + lanes_alarms_ary.insert(lane, self.regs.peek16(addr) & 0xFF0F) + self.log.trace("Lane {} alarms rb: 0x{:X}".format(lane, lanes_alarms_ary[lane])) + lane += 1 + + enable_analog_output = True + # Report warnings based on an error matrix (register_width * lanes). + errors_ary = [] + for error in range(0, 16): + errors_ary.insert(error, []) + # Extract errors from lanes. + for lane in range(0, len(lanes_alarms_ary)): + if lanes_alarms_ary[lane] & (0b1 << error) > 0: + errors_ary[error].append(lane) + if len(errors_ary[error]) > 0: + enable_analog_output = False + self.log.warning(ALARM_ERRORS_DESCRIPTION[error] + + " in lane(s): " + ' '.join(map(str, errors_ary[error]))) + + self.tx_enable(enable_analog_output) + self.log.debug("%s analog TX output.", + {True: 'Enabling', False: 'Disabling'}[enable_analog_output]) + return enable_analog_output + + + def test_mode(self, mode='PRBS-31', lane=0): + """ + This method enables the DAC's test mode to verify the SerDes. + Users should monitor the ALARM pin to see the results of the test. + If the test is failing, ALARM will be high (or toggling if marginal). + If the test is passing, the ALARM will be low. + """ + MODE_VAL = {'OFF': 0x0, 'PRBS-7': 0x2, 'PRBS-23': 0x3, 'PRBS-31': 0x4} + assert mode.upper() in MODE_VAL + assert lane in range(0, 8) + self.log.debug("Setting test mode for lane {} at the DAC: {}.".format(lane, mode)) + # To run the PRBS test on the DAC, users first need to setup the DAC for + # normal use, then make the following SPI writes: + # 1. config74, set bits 4:0 to 0x1E to disable JESD clock. + addr = 0x4A + rb = self.regs.peek16(addr) + data_w = (rb & ~0x001F) | 0x001E if mode != 'OFF' else 0x0F3E + self.log.trace("Writing register {:02X} with {:04X}".format(addr, data_w)) + self.regs.poke16(addr, data_w) + # 2. config61, set bits 14:12 to 0x2 to enable the 7-bit PRBS test pattern; or + # set bits 14:12 to 0x3 to enable the 23-bit PRBS test pattern; or + # set bits 14:12 to 0x4 to enable the 31-bit PRBS test pattern. + addr = 0x3D + rb = self.regs.peek16(addr) + data_w = (rb & ~0x7000) | (MODE_VAL[mode] << 12) + self.log.trace("Writing register {:02X} with {:04X}".format(addr, data_w)) + self.regs.poke16(addr, data_w) + # 3. config27, set bits 11:8 to 0x3 to output PRBS testfail on ALARM pin. + # 4. config27, set bits 14:12 to the lane to be tested (0 through 7). + addr = 0x1B + rb = self.regs.peek16(addr) + data_w = (rb & ~0x7F00) | (0x3 << 8) | (lane << 12) if mode != 'OFF' else 0x0000 + self.log.trace("Writing register {:02X} with {:04X}".format(addr, data_w)) + self.regs.poke16(addr, data_w) + # 5. config62, make sure bits 12:11 are set to 0x0 to disable character alignment. + addr = 0x3E + rb = self.regs.peek16(addr) + if ((rb & 0x58) >> 11) != 0x0: + self.log.error("Char alignment is enabled when not expected.") diff --git a/mpm/python/usrp_mpm/dboard_manager/gain_rh.py b/mpm/python/usrp_mpm/dboard_manager/gain_rh.py new file mode 100644 index 000000000..978b8158b --- /dev/null +++ b/mpm/python/usrp_mpm/dboard_manager/gain_rh.py @@ -0,0 +1,93 @@ +# +# Copyright 2018 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +Gain table control for Rhodium +""" + +from __future__ import print_function +from usrp_mpm.dboard_manager.gaintables_rh import RX_LOWBAND_GAIN_TABLE +from usrp_mpm.dboard_manager.gaintables_rh import RX_HIGHBAND_GAIN_TABLE +from usrp_mpm.dboard_manager.gaintables_rh import TX_LOWBAND_GAIN_TABLE +from usrp_mpm.dboard_manager.gaintables_rh import TX_HIGHBAND_GAIN_TABLE + +#from usrp_mpm.dboard_manager.rhodium import Rhodium + +############################################################################### +# Constants +############################################################################### + +GAIN_TABLE_MIN_INDEX = 0 +GAIN_TABLE_MAX_INDEX = 60 +DSA1_MIN_INDEX = 0 +DSA1_MAX_INDEX = 30 +DSA2_MIN_INDEX = 0 +DSA2_MAX_INDEX = 30 + +GAIN_TBL_SEL_ADDR = 6 +GAIN_TBL_SEL_TX_SHIFT = 8 +GAIN_TBL_SEL_RX_SHIFT = 0 +GAIN_TBL_SEL_HIGH_BAND = 1 +GAIN_TBL_SEL_LOW_BAND = 0 + +# convenience data values for GAIN_TBL_SEL +GAIN_TBL_SEL_DATA_BOTH_HIGH = \ + (GAIN_TBL_SEL_HIGH_BAND << GAIN_TBL_SEL_TX_SHIFT) | \ + (GAIN_TBL_SEL_HIGH_BAND << GAIN_TBL_SEL_RX_SHIFT) +GAIN_TBL_SEL_DATA_BOTH_LOW = \ + (GAIN_TBL_SEL_LOW_BAND << GAIN_TBL_SEL_TX_SHIFT) | \ + (GAIN_TBL_SEL_LOW_BAND << GAIN_TBL_SEL_RX_SHIFT) + +############################################################################### +# Main class +############################################################################### + +class GainTableRh(): + """ + CPLD gain table loader for Rhodium daughterboards + """ + def __init__(self, cpld_regs, gain_tbl_regs, parent_log=None): + self.log = parent_log.getChild("CPLDGainTbl") if parent_log is not None \ + else get_logger("CPLDGainTbl") + self.cpld_regs = cpld_regs + self.gain_tbl_regs = gain_tbl_regs + assert hasattr(self.cpld_regs, 'poke16') + assert hasattr(self.gain_tbl_regs, 'poke16') + + def _load_default_table(self, table, gain_table): + def _create_spi_loader_message(table, index, dsa1, dsa2): + addr = 0 + data = 0 + if table == "rx": + tableindex = 1 + elif table == "tx": + tableindex = 2 + else: + raise RuntimeError("Invalid table selected in gain loader: " + table) + addr |= (tableindex << 6) + addr |= (index << 0) + data |= (dsa1 << 5) + data |= (dsa2 << 0) + return addr, data + for i in range(GAIN_TABLE_MIN_INDEX, GAIN_TABLE_MAX_INDEX): + addr, data = _create_spi_loader_message( + table, + i, + gain_table[i][0], + gain_table[i][1]) + self.gain_tbl_regs.poke16(addr, data) + + def init(self): + """ + Loads the default gain table values to the CPLD via SPI + """ + self.log.trace("Loading gain tables to CPLD") + self.cpld_regs.poke16(GAIN_TBL_SEL_ADDR, GAIN_TBL_SEL_DATA_BOTH_HIGH) + self._load_default_table("rx", RX_HIGHBAND_GAIN_TABLE) + self._load_default_table("tx", TX_HIGHBAND_GAIN_TABLE) + self.cpld_regs.poke16(GAIN_TBL_SEL_ADDR, GAIN_TBL_SEL_DATA_BOTH_LOW) + self._load_default_table("rx", RX_LOWBAND_GAIN_TABLE) + self._load_default_table("tx", TX_LOWBAND_GAIN_TABLE) + self.log.trace("Gain tables loaded") diff --git a/mpm/python/usrp_mpm/dboard_manager/gaintables_rh.py b/mpm/python/usrp_mpm/dboard_manager/gaintables_rh.py new file mode 100644 index 000000000..9e2178006 --- /dev/null +++ b/mpm/python/usrp_mpm/dboard_manager/gaintables_rh.py @@ -0,0 +1,271 @@ +# +# Copyright 2018 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +Gain table constants for Rhodium +""" + +############################################################################### +# Constants +############################################################################### + +# Rhodium has two configurable gain elements +# DSA1 - 0-30, attenuation +# DSA2 - 0-30, attenuation +# Gain table values are written as [DSA1, DSA2] +# This is stored as a gain table, so index 0 is the lowest available power +# These default gain tables have 61 indices + +RX_LOWBAND_GAIN_TABLE = [ + [30, 30], + [29, 30], + [28, 30], + [27, 30], + [26, 30], + [25, 30], + [24, 30], + [23, 30], + [22, 30], + [21, 30], + [20, 30], + [19, 30], + [18, 30], + [17, 30], + [16, 30], + [15, 30], + [14, 30], + [13, 30], + [12, 30], + [11, 30], + [10, 30], + [ 9, 30], + [ 9, 29], + [ 8, 29], + [ 8, 28], + [ 7, 28], + [ 7, 27], + [ 6, 27], + [ 6, 26], + [ 5, 26], + [ 5, 25], + [ 5, 24], + [ 5, 23], + [ 5, 22], + [ 5, 21], + [ 5, 20], + [ 5, 19], + [ 5, 18], + [ 5, 17], + [ 5, 16], + [ 5, 15], + [ 4, 15], + [ 4, 14], + [ 3, 14], + [ 3, 13], + [ 2, 13], + [ 2, 12], + [ 1, 12], + [ 1, 11], + [ 0, 11], + [ 0, 10], + [ 0, 9], + [ 0, 8], + [ 0, 7], + [ 0, 6], + [ 0, 5], + [ 0, 4], + [ 0, 3], + [ 0, 2], + [ 0, 1], + [ 0, 0]] + +RX_HIGHBAND_GAIN_TABLE = [ + [30, 30], + [29, 30], + [28, 30], + [27, 30], + [26, 30], + [25, 30], + [24, 30], + [23, 30], + [22, 30], + [21, 30], + [20, 30], + [19, 30], + [18, 30], + [17, 30], + [16, 30], + [15, 30], + [14, 30], + [13, 30], + [12, 30], + [11, 30], + [10, 30], + [ 9, 30], + [ 8, 30], + [ 7, 30], + [ 7, 29], + [ 6, 29], + [ 5, 29], + [ 5, 28], + [ 4, 28], + [ 3, 28], + [ 3, 27], + [ 2, 27], + [ 2, 26], + [ 2, 25], + [ 1, 25], + [ 1, 24], + [ 1, 23], + [ 0, 23], + [ 0, 22], + [ 0, 21], + [ 0, 20], + [ 0, 19], + [ 0, 18], + [ 0, 17], + [ 0, 16], + [ 0, 15], + [ 0, 14], + [ 0, 13], + [ 0, 12], + [ 0, 11], + [ 0, 10], + [ 0, 9], + [ 0, 8], + [ 0, 7], + [ 0, 6], + [ 0, 5], + [ 0, 4], + [ 0, 3], + [ 0, 2], + [ 0, 1], + [ 0, 0]] + +TX_LOWBAND_GAIN_TABLE = [ + [30, 30], + [29, 30], + [29, 29], + [28, 29], + [28, 28], + [27, 28], + [27, 27], + [26, 27], + [26, 26], + [25, 26], + [25, 25], + [24, 25], + [24, 24], + [23, 24], + [23, 23], + [22, 23], + [22, 22], + [21, 22], + [21, 21], + [20, 21], + [20, 20], + [19, 20], + [19, 19], + [18, 19], + [18, 18], + [17, 18], + [17, 17], + [16, 17], + [16, 16], + [15, 16], + [15, 15], + [14, 15], + [14, 14], + [13, 14], + [13, 13], + [12, 13], + [12, 12], + [11, 12], + [11, 11], + [10, 11], + [10, 10], + [ 9, 10], + [ 9, 9], + [ 8, 9], + [ 8, 8], + [ 7, 8], + [ 7, 7], + [ 6, 7], + [ 6, 6], + [ 5, 6], + [ 5, 5], + [ 4, 5], + [ 4, 4], + [ 3, 4], + [ 3, 3], + [ 2, 3], + [ 2, 2], + [ 1, 2], + [ 1, 1], + [ 0, 1], + [ 0, 0]] + +TX_HIGHBAND_GAIN_TABLE = [ + [30, 30], + [29, 30], + [29, 29], + [28, 29], + [28, 28], + [27, 28], + [27, 27], + [26, 27], + [26, 26], + [25, 26], + [25, 25], + [24, 25], + [24, 24], + [23, 24], + [23, 23], + [22, 23], + [22, 22], + [21, 22], + [21, 21], + [20, 21], + [20, 20], + [19, 20], + [19, 19], + [18, 19], + [18, 18], + [17, 18], + [17, 17], + [16, 17], + [16, 16], + [15, 16], + [15, 15], + [14, 15], + [14, 14], + [13, 14], + [13, 13], + [12, 13], + [12, 12], + [11, 12], + [11, 11], + [10, 11], + [10, 10], + [ 9, 10], + [ 9, 9], + [ 8, 9], + [ 8, 8], + [ 7, 8], + [ 7, 7], + [ 6, 7], + [ 6, 6], + [ 5, 6], + [ 5, 5], + [ 4, 5], + [ 4, 4], + [ 3, 4], + [ 3, 3], + [ 2, 3], + [ 2, 2], + [ 1, 2], + [ 1, 1], + [ 0, 1], + [ 0, 0]] diff --git a/mpm/python/usrp_mpm/dboard_manager/lmk_rh.py b/mpm/python/usrp_mpm/dboard_manager/lmk_rh.py new file mode 100644 index 000000000..288f03864 --- /dev/null +++ b/mpm/python/usrp_mpm/dboard_manager/lmk_rh.py @@ -0,0 +1,316 @@ +# +# Copyright 2018 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +LMK04828 driver for use with Rhodium +""" + +import time +from ..mpmlog import get_logger +from ..chips import LMK04828 + +class LMK04828Rh(LMK04828): + """ + This class provides an interface to configure the LMK04828 IC through SPI. + """ + + def __init__(self, slot_idx, regs_iface, ref_clock_freq, sampling_clock_freq, parent_log=None): + self.log = \ + parent_log.getChild("LMK04828-{}".format(slot_idx)) if parent_log is not None \ + else get_logger("LMK04828-{}".format(slot_idx)) + LMK04828.__init__(self, regs_iface, parent_log) + self.log.debug("Using reference clock frequency {} MHz".format(ref_clock_freq/1e6)) + self.log.debug("Using sampling clock frequency: {} MHz".format(sampling_clock_freq/1e6)) + self.ref_clock_freq = ref_clock_freq + self.sampling_clock_freq = sampling_clock_freq + # VCXO on Rh runs at 122.88 MHz + self.vcxo_freq = 122.88e6 + self.clkin_r_divider = { 10e6: 250, 20e6: 500, 25e6: 625, 30.72e6: 750}[self.ref_clock_freq] + self.pll1_n_divider = { 10e6: 3072, 20e6: 3072, 25e6: 3072, 30.72e6: 3000}[self.ref_clock_freq] + self.pll2_r_divider = {400e6: 32, 491.52e6: 32, 500e6: 128}[self.sampling_clock_freq] + self.pll2_prescaler = {400e6: 5, 491.52e6: 2, 500e6: 5}[self.sampling_clock_freq] + self.pll2_n_divider = {400e6: 125, 491.52e6: 320, 500e6: 625}[self.sampling_clock_freq] + self.pll2_vco_freq = (self.vcxo_freq/self.pll2_r_divider)*self.pll2_prescaler*self.pll2_n_divider + self.vco_selection = self.get_vco_selection() + self.sysref_divider = {400e6: 144, 491.52e6: 120, 500e6: 144}[self.sampling_clock_freq] + self.fpga_sysref_dly = {400e6: 11, 491.52e6: 8, 500e6: 10}[self.sampling_clock_freq] + self.clkout_divider = {400e6: 6, 491.52e6: 5, 500e6: 6}[self.sampling_clock_freq] + self.lb_lo_divider = {400e6: 2, 491.52e6: 2, 500e6: 2}[self.sampling_clock_freq] + self.log.trace("Variable Configuration Report: " + "clkin0/1_r = 0d{}, clkout_div = 0d{}, pll1_n = 0d{}" + .format(self.clkin_r_divider, self.clkout_divider, self.pll1_n_divider)) + self.log.trace("Variable Configuration Report: " + "sysref_divider = 0d{}, fpga_sysref_dly = 0d{}," + .format(self.sysref_divider, self.fpga_sysref_dly)) + self.log.trace("Variable Configuration Report: " + "pll2_pre = 0d{}, pll2_n = 0d{}, pll2_vco_freq = 0d{}" + .format(self.pll2_prescaler, self.pll2_n_divider, self.pll2_vco_freq)) + + self.init() + self.config() + + def get_vco_freq(self): + """ + Return the calculated VCO frequency in the LMK PLL2. + """ + return self.pll2_vco_freq + + + def get_vco_selection(self): + """ + The internal VCO (0/1) is selected depending on the pll2_vco_freq value. + According to the datasheet: + VCO0 Frequency -> 2370 to 2630 MHz (Default) + VCO1 Frequency -> 2920 to 3080 MHz + The VCO selection is configured with bits 6:5 (VCO_MUX) in register 0x138: + 0x00 | VCO 0 + 0x01 | VCO 1 + 0x01 | CLKin1 (ext) + 0x03 | Reserved + This function returns the full register value with ONLY the VCO selected + (i.e. the returned value must be or-ed with the rest of the configuration). + """ + if (self.pll2_vco_freq >= 2370e6) and (self.pll2_vco_freq <= 2630e6): + self.log.trace("VCO0 selected for PLL2.") + return 0x00 << 5 + elif (self.pll2_vco_freq >= 2920e6) and (self.pll2_vco_freq <= 3080e6): + self.log.trace("Internal VCO1 selected for PLL2.") + return 0x01 << 5 + else: + self.log.error("The calculated PLL2 VCO frequency ({}) is not supported \ + by neither internal VCO".format(self.pll2_vco_freq)) + raise Exception("PLL2 VCO frequency not supported. Check log for details.") + + + def init(self): + """ + Basic init. Turns it on. Enables SPI reads. + """ + self.log.debug("Reset LMK & Verify") + self.pokes8(( + (0x000, 0x80), # Assert reset + (0x000, 0x00), # De-assert reset + (0x002, 0x00), # De-assert power down + )) + if not self.verify_chip_id(): + raise Exception("Unable to locate LMK04828") + + + def config(self): + """ + Writes the entire configuration necessary for Rhodium operation. + """ + self.log.trace("LMK Initialization") + + # The sampling clocks going to the converters must be set at the sampling_clock_freq + # rate. But, the JESD204B IP at the FPGA expects the clocks to be half that rate; + # therefore, the actual divider for these clocks must be twice the normal. + convclk_div_val = self.divide_to_reg(self.clkout_divider) + convclk_cnt_val = self.divide_to_cnth_cntl_reg(self.clkout_divider) + fpgaclk_div_val = self.divide_to_reg(self.clkout_divider*2) + fpgaclk_cnt_val = self.divide_to_cnth_cntl_reg(self.clkout_divider*2) + # The LMK provides both the TX and RX low-band references, which are configured + # with the same divider values. + lb_lo_div_val = self.divide_to_reg(self.lb_lo_divider) + lb_lo_cnt_val = self.divide_to_cnth_cntl_reg(self.lb_lo_divider) + + # Determine one of the SDCLkOut configuration registers (0x104) for the SYSREF + # signal going to the FPGA. + # SYSREF out VCO cycles to delay SYSREF + fpga_sysref_config = (0b1 << 5) | ((self.fpga_sysref_dly - 1) << 1) + self.log.trace("FPGA SYSREF delay register (0x104): 0x{:02X}".format(fpga_sysref_config)) + + self.pokes8(( + (0x100, fpgaclk_div_val), # CLKout Config (FPGA Clock) + (0x101, fpgaclk_cnt_val), # CLKout Config + (0x102, 0x88), # CLKout Config + (0x103, 0x00), # CLKout Config + (0x104, fpga_sysref_config), # CLKout Config + (0x105, 0x00), # CLKout Config + (0x106, 0x72), # CLKout Config + (0x107, 0x11), # CLKout Config + (0x108, fpgaclk_div_val), # CLKout Config (MGT Reference Clock) + (0x109, fpgaclk_cnt_val), # CLKout Config + (0x10A, 0x88), # CLKout Config + (0x10B, 0x00), # CLKout Config + (0x10C, 0x00), # CLKout Config + (0x10D, 0x00), # CLKout Config + (0x10E, 0xF1), # CLKout Config + (0x10F, 0x05), # CLKout Config + (0x110, convclk_div_val), # CLKout Config (DAC Clock) + (0x111, convclk_cnt_val), # CLKout Config + (0x112, 0x22), # CLKout Config + (0x113, 0x00), # CLKout Config + (0x114, 0x20), # CLKout Config + (0x115, 0x00), # CLKout Config + (0x116, 0x72), # CLKout Config + (0x117, 0x75), # CLKout Config + (0x118, lb_lo_div_val), # CLKout Config (TX LB LO) + (0x119, lb_lo_cnt_val), # CLKout Config + (0x11A, 0x11), # CLKout Config + (0x11B, 0x00), # CLKout Config + (0x11C, 0x00), # CLKout Config + (0x11D, 0x00), # CLKout Config + (0x11E, 0x71), # CLKout Config + (0x11F, 0x05), # CLKout Config + (0x120, fpgaclk_div_val), # CLKout Config (Test Point Clock) + (0x121, fpgaclk_cnt_val), # CLKout Config + (0x122, 0x22), # CLKout Config + (0x123, 0x00), # CLKout Config + (0x124, 0x20), # CLKout Config + (0x125, 0x00), # CLKout Config + (0x126, 0x72), # CLKout Config + (0x127, 0x15), # CLKout Config + (0x128, lb_lo_div_val), # CLKout Config (RX LB LO) + (0x129, lb_lo_cnt_val), # CLKout Config + (0x12A, 0x22), # CLKout Config + (0x12B, 0x00), # CLKout Config + (0x12C, 0x00), # CLKout Config + (0x12D, 0x00), # CLKout Config + (0x12E, 0x71), # CLKout Config + (0x12F, 0x85), # CLKout Config + (0x130, convclk_div_val), # CLKout Config (ADC Clock) + (0x131, convclk_cnt_val), # CLKout Config + (0x132, 0x22), # CLKout Config + (0x133, 0x00), # CLKout Config + (0x134, 0x20), # CLKout Config + (0x135, 0x00), # CLKout Config + (0x136, 0x72), # CLKout Config + (0x137, 0x55), # CLKout Config + (0x138, (0x04 | self.vco_selection)), # VCO_MUX to VCO 0; OSCin->OSCout @ LVPECL 1600 mV + (0x139, 0x00), # SYSREF Source = MUX; SYSREF MUX = Normal SYNC + (0x13A, (self.sysref_divider & 0x1F00) >> 8), # SYSREF Divide [12:8] + (0x13B, (self.sysref_divider & 0x00FF) >> 0), # SYSREF Divide [7:0] + (0x13C, 0x00), # SYSREF DDLY [12:8] + (0x13D, 0x0A), # SYSREF DDLY [7:0] ... 8 is default, <8 is reserved + (0x13E, 0x00), # SYSREF Pulse Count = 1 pulse/request + (0x13F, 0x00), # Feedback Mux: Disabled. OSCin, drives PLL1N divider (Dual PLL non 0-delay). PLL2_P drives PLL2N divider. + (0x140, 0x00), # POWERDOWN options + (0x141, 0x00), # Dynamic digital delay enable + (0x142, 0x00), # Dynamic digital delay step + (0x143, 0xD1), # SYNC edge sensitive; SYSREF_CLR; SYNC Enabled; SYNC from pin no pulser + (0x144, 0x00), # Enable SYNC on all outputs including sysref + (0x145, 0x7F), # Always program to d127 + (0x146, 0x00), # CLKin Type & En + (0x147, 0x0A), # CLKin_SEL = CLKin0 manual (10/20/25 MHz) / CLKin1 manual (30.72 MHz); CLKin0/1 to PLL1 + (0x148, 0x02), # CLKin_SEL0 = input /w pulldown (default) + (0x149, 0x02), # CLKin_SEL1 = input w/ pulldown +SDIO RDBK (push-pull) + (0x14A, 0x02), # RESET type: in. w/ pulldown (default) + (0x14B, 0x02), # Holdover & DAC Manual Mode + (0x14C, 0x00), # DAC Manual Mode + (0x14D, 0x00), # DAC Settings (defaults) + (0x14E, 0x00), # DAC Settings (defaults) + (0x14F, 0x7F), # DAC Settings (defaults) + (0x150, 0x00), # Holdover Settings; bit 1 = '0' per long PLL1 lock time debug + (0x151, 0x02), # Holdover Settings (defaults) + (0x152, 0x00), # Holdover Settings (defaults) + (0x153, (self.clkin_r_divider & 0x3F00) >> 8), # CLKin0_R divider [13:8], default = 0 + (0x154, (self.clkin_r_divider & 0x00FF) >> 0), # CLKin0_R divider [7:0], default = d120 + (0x155, (self.clkin_r_divider & 0x3F00) >> 8), # CLKin1_R divider [13:8], default = 0 + (0x156, (self.clkin_r_divider & 0x00FF) >> 0), # CLKin1_R divider [7:0], default = d150 + (0x157, 0x00), # CLKin2_R divider [13:8], default = 0 (Not used) + (0x158, 0x01), # CLKin2_R divider [7:0], default = d1 (Not used) + (0x159, (self.pll1_n_divider & 0x3F00) >> 8), # PLL1 N divider [13:8], default = d6 + (0x15A, (self.pll1_n_divider & 0x00FF) >> 0), # PLL1 N divider [7:0], default = d0 + (0x15B, 0xC7), # PLL1 PFD: negative slope for active filter / CP = 750 uA + (0x15C, 0x27), # PLL1 DLD Count [13:8] + (0x15D, 0x10), # PLL1 DLD Count [7:0] + (0x15E, 0x00), # PLL1 R/N delay, defaults = 0 + (0x15F, 0x0B), # Status LD1 pin = PLL1 LD, push-pull output + (0x160, (self.pll2_r_divider & 0x0F00) >> 8), # PLL2 R divider [11:8]; + (0x161, (self.pll2_r_divider & 0x00FF) >> 0), # PLL2 R divider [7:0] + (0x162, self.pll2_pre_to_reg(self.pll2_prescaler)), # PLL2 prescaler; OSCin freq + (0x163, 0x00), # PLL2 Cal = PLL2 normal val + (0x164, 0x00), # PLL2 Cal = PLL2 normal val + (0x165, 0x0A), # PLL2 Cal = PLL2 normal val + (0x171, 0xAA), # Write this val after x165 + (0x172, 0x02), # Write this val after x165 + (0x17C, 0x15), # VCo1 Cal; write before x168 + (0x17D, 0x33), # VCo1 Cal; write before x168 + (0x166, (self.pll2_n_divider & 0x030000) >> 16), # PLL2 N[17:16] + (0x167, (self.pll2_n_divider & 0x00FF00) >> 8), # PLL2 N[15:8] + (0x168, (self.pll2_n_divider & 0x0000FF) >> 0), # PLL2 N[7:0] + (0x169, 0x59), # PLL2 PFD + (0x16A, 0x27), # PLL2 DLD Count [13:8] = default d39; SYSREF_REQ_EN disabled. + (0x16B, 0x10), # PLL2 DLD Count [7:0] = default d16 + (0x16C, 0x00), # PLL2 Loop filter R3/4 = 200 ohm + (0x16D, 0x00), # PLL2 loop filter C3/4 = 10 pF + (0x16E, 0x13), # Status LD2 pin = Output push-pull, PLL2 DLD + (0x173, 0x00), # Do not power down PLL2 or prescaler + )) + + # Poll for PLL1/2 lock. Total time = 10 * 100 ms = 1000 ms max. + self.log.trace("Polling for PLL lock...") + locked = False + for _ in range(10): + time.sleep(0.100) + # Clear stickies + self.pokes8(( + (0x182, 0x1), # Clear Lock Detect Sticky + (0x182, 0x0), # Clear Lock Detect Sticky + (0x183, 0x1), # Clear Lock Detect Sticky + (0x183, 0x0), # Clear Lock Detect Sticky + )) + if self.check_plls_locked(): + locked = True + self.log.trace("LMK PLLs Locked!") + break + if not locked: + raise RuntimeError("At least one LMK PLL did not lock! Check the logs for details.") + + self.log.trace("Setting SYNC and SYSREF config...") + self.pokes8(( + (0x143, 0xF1), # toggle SYNC polarity to trigger SYNC event + (0x143, 0xD1), # toggle SYNC polarity to trigger SYNC event + (0x139, 0x02), # SYSREF Source = MUX; SYSREF MUX = pulser + (0x144, 0xFF), # Disable SYNC on all outputs including sysref + (0x143, 0x52), # Pulser selected; SYNC enabled; 1 shot enabled + )) + self.log.info("LMK initialized and locked!") + + def lmk_shift(self, num_shifts=0): + """ + Apply time shift + """ + self.log.trace("Clock Shifting Commencing using Dynamic Digital Delay...") + # The sampling clocks going to the converters are set at the sampling_clock_freq + # rate. But, the JESD204B IP at the FPGA expects the clocks to be half that rate; + # therefore, the actual divider for the FPGA clocks is twice the normal. + ddly_value_conv = self.divide_to_cnth_cntl_reg(self.clkout_divider+1) \ + if num_shifts >= 0 else self.divide_to_cnth_cntl_reg(self.clkout_divider-1) + ddly_value_fpga = self.divide_to_cnth_cntl_reg(self.clkout_divider*2+1) \ + if num_shifts >= 0 else self.divide_to_cnth_cntl_reg(self.clkout_divider*2-1) + ddly_value_sysref = self.sysref_divider+1 if num_shifts >= 0 else self.sysref_divider-1 + # Since the LMK provides the low-band LO references for RX/TX, these need to be + # also shifted along with the sampling clocks to achieve the best possible + # phase alignment perfomance at this band. + ddly_value_lb_lo = self.divide_to_cnth_cntl_reg(self.lb_lo_divider+1) \ + if num_shifts >= 0 else self.divide_to_cnth_cntl_reg(self.lb_lo_divider-1) + self.pokes8(( + # Clocks to shift: 0(FPGA CLK), 4(DAC), 6(TX LB-LO), 8(Test), 10(RX LB-LO), 12(ADC) + (0x141, 0xFD), # Dynamic digital delay enable on outputs. + (0x143, 0x53), # SYSREF_CLR; SYNC Enabled; SYNC from pulser @ regwrite + (0x139, 0x02), # SYSREF_MUX = Pulser + (0x101, ddly_value_fpga), # Set DDLY values for DCLKout0 +/-1 on low cnt. + (0x102, ddly_value_fpga), # Hidden register. Write the same as previous based on inc/dec. + (0x111, ddly_value_conv), # Set DDLY values for DCLKout4 +/-1 on low cnt + (0x112, ddly_value_conv), # Hidden register. Write the same as previous based on inc/dec. + (0x119, ddly_value_lb_lo), # Set DDLY values for DCLKout6 +/-1 on low cnt + (0x11A, ddly_value_lb_lo), # Hidden register. Write the same as previous based on inc/dec. + (0x121, ddly_value_fpga), # Set DDLY values for DCLKout8 +/-1 on low cnt + (0x122, ddly_value_fpga), # Hidden register. Write the same as previous based on inc/dec. + (0x129, ddly_value_lb_lo), # Set DDLY values for DCLKout10 +/-1 on low cnt + (0x12A, ddly_value_lb_lo), # Hidden register. Write the same as previous based on inc/dec. + (0x131, ddly_value_conv), # Set DDLY values for DCLKout12 +/-1 on low cnt + (0x132, ddly_value_conv), # Hidden register. Write the same as previous based on inc/dec. + (0x13C, (ddly_value_sysref & 0x1F00) >> 8), # SYSREF DDLY value + (0x13D, (ddly_value_sysref & 0x00FF) >> 0), # SYSREF DDLY value + (0x144, 0x02), # Enable SYNC on outputs 0, 4, 6, 8, 10, 12 + )) + for _ in range(abs(num_shifts)): + self.poke8(0x142, 0x1) + # Put everything back the way it was before shifting. + self.poke8(0x144, 0xFF) # Disable SYNC on all outputs including SYSREF + self.poke8(0x143, 0x52) # Pulser selected; SYNC enabled; 1 shot enabled 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() diff --git a/mpm/python/usrp_mpm/dboard_manager/rh_periphs.py b/mpm/python/usrp_mpm/dboard_manager/rh_periphs.py new file mode 100644 index 000000000..e9b171c5e --- /dev/null +++ b/mpm/python/usrp_mpm/dboard_manager/rh_periphs.py @@ -0,0 +1,193 @@ +# +# Copyright 2018 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +Rhodium dboard peripherals (CPLD, port expander, dboard regs) +""" + + +import time +from usrp_mpm.sys_utils.sysfs_gpio import SysFSGPIO, GPIOBank +from usrp_mpm.mpmutils import poll_with_timeout + + +class TCA6408(object): + """ + Abstraction layer for the port/gpio expander + """ + pins = ( + 'PWR-GOOD-3.6V', #3.6V + 'PWR-GOOD-1.1V', #1.1V + 'PWR-GOOD-2.0V', #2.0V + 'PWR-GOOD-5.4V', #5.4V + 'PWR-GOOD-5.5V', #6.8V + ) + + def __init__(self, i2c_dev): + if i2c_dev is None: + raise RuntimeError("Need to specify i2c device to use the TCA6408") + self._gpios = SysFSGPIO({'label': 'tca6408'}, 0x3F, 0x00, 0x00, i2c_dev) + + def set(self, name, value=None): + """ + Assert a pin by name + """ + assert name in self.pins + self._gpios.set(self.pins.index(name), value=value) + + def reset(self, name): + """ + Deassert a pin by name + """ + self.set(name, value=0) + + def get(self, name): + """ + Read back a pin by name + """ + assert name in self.pins + return self._gpios.get(self.pins.index(name)) + + +class FPGAtoDbGPIO(GPIOBank): + """ + Abstraction layer for the FPGA to Daughterboard GPIO + """ + EMIO_BASE = 54+8 + DB_POWER_ENABLE = 0 + RF_POWER_ENABLE = 1 + + def __init__(self, slot_idx): + pwr_base = self.EMIO_BASE + 2*slot_idx + GPIOBank.__init__( + self, + {'label': 'zynq_gpio'}, + pwr_base, + 0x3, # use_mask + 0x3, # ddr + ) + + +class RhCPLD(object): + """ + Control class for the CPLD. + """ + + REG_SIGNATURE = 0x0000 + REG_MINOR_REVISION = 0x0001 + REG_MAJOR_REVISION = 0x0002 + REG_BUILD_CODE_LSB = 0x0003 + REG_BUILD_CODE_MSB = 0x0004 + REG_SCRATCH = 0x0005 + REG_GAIN_TABLE_SEL = 0x0006 + REG_DAC_ALARM = 0x0007 + + CPLD_SIGNATURE = 0x0045 + CPLD_MAJOR_REV = 4 + CPLD_MINOR_REV = 0 + + def __init__(self, regs, log): + """ + Initialize communication with the Rh CPLD + """ + self.log = log.getChild("CPLD") + self.log.debug("Initializing CPLD...") + self.cpld_regs = regs + self.poke16 = self.cpld_regs.peek16 + self.peek16 = self.cpld_regs.peek16 + # According to the datasheet, The CPLD's internal configuration time can take + # anywhere from 0.6 to 3.4 ms. Until then, any ifc accesses will be invalid, + # We will blind wait 5 ms and check the signature register to verify operation + time.sleep(0.005) + signature = self.peek16(self.REG_SIGNATURE) + if self.CPLD_SIGNATURE != signature: + self.log.error("CPLD Signature Mismatch! " \ + "Expected: 0x{:04X} Got: 0x{:04X}".format(self.CPLD_SIGNATURE, signature)) + raise RuntimeError("CPLD Signature Check Failed! " + "Incorrect signature readback.") + minor_rev = self.peek16(self.REG_MINOR_REVISION) + major_rev = self.peek16(self.REG_MAJOR_REVISION) + if major_rev != self.CPLD_MAJOR_REV: + self.log.error( + "CPLD Major Revision check mismatch! Expected: %d Got: %d", + self.CPLD_MAJOR_REV, + major_rev + ) + raise RuntimeError("CPLD Revision Check Failed! MPM is not " + "compatible with the loaded CPLD image.") + date_code = self.peek16(self.REG_BUILD_CODE_LSB) | \ + (self.peek16(self.REG_BUILD_CODE_MSB) << 16) + self.log.debug( + "CPLD Signature: 0x{:04X} " + "Revision: {}.{} " + "Date code: 0x{:08X}" + .format(signature, major_rev, minor_rev, date_code)) + + def get_dac_alarm(self): + """ + This function polls and returns the DAC's ALARM signal connected to the CPLD. + """ + return (self.peek16(self.REG_DAC_ALARM) & 0x0001) + + # TODO: add more control/status functionality to this class? + + +class DboardClockControl(object): + """ + Control the FPGA MMCM for Radio Clock control. + """ + # Clocking Register address constants + RADIO_CLK_MMCM = 0x0020 + PHASE_SHIFT_CONTROL = 0x0024 + RADIO_CLK_ENABLES = 0x0028 + MGT_REF_CLK_STATUS = 0x0030 + + def __init__(self, regs, log): + self.log = log + self.regs = regs + self.poke32 = self.regs.poke32 + self.peek32 = self.regs.peek32 + + def enable_outputs(self, enable=True): + """ + Enables or disables the MMCM outputs. + """ + if enable: + self.poke32(self.RADIO_CLK_ENABLES, 0x011) + else: + self.poke32(self.RADIO_CLK_ENABLES, 0x000) + + def reset_mmcm(self): + """ + Uninitialize and reset the MMCM + """ + self.log.trace("Disabling all Radio Clocks, then resetting MMCM...") + self.enable_outputs(False) + self.poke32(self.RADIO_CLK_MMCM, 0x1) + + def enable_mmcm(self): + """ + Unreset MMCM and poll lock indicators + + If MMCM is not locked after unreset, an exception is thrown. + """ + self.log.trace("Un-resetting MMCM...") + self.poke32(self.RADIO_CLK_MMCM, 0x2) + if not poll_with_timeout( + lambda: bool(self.peek32(self.RADIO_CLK_MMCM) & 0x10), + 500, + 10, + ): + self.log.error("MMCM not locked!") + raise RuntimeError("MMCM not locked!") + self.log.trace("MMCM locked. Enabling output MMCM clocks...") + self.enable_outputs(True) + + def check_refclk(self): + """ + Not technically a clocking reg, but related. + """ + return bool(self.peek32(self.MGT_REF_CLK_STATUS) & 0x1) + diff --git a/mpm/python/usrp_mpm/dboard_manager/rhodium.py b/mpm/python/usrp_mpm/dboard_manager/rhodium.py new file mode 100644 index 000000000..81ca221a7 --- /dev/null +++ b/mpm/python/usrp_mpm/dboard_manager/rhodium.py @@ -0,0 +1,573 @@ +# +# Copyright 2018 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +Rhodium dboard implementation module +""" + +from __future__ import print_function +import os +import threading +from six import iterkeys, iteritems +from usrp_mpm import lib # Pulls in everything from C++-land +from usrp_mpm.dboard_manager import DboardManagerBase +from usrp_mpm.dboard_manager.rh_periphs import TCA6408, FPGAtoDbGPIO +from usrp_mpm.dboard_manager.rh_init import RhodiumInitManager +from usrp_mpm.dboard_manager.rh_periphs import RhCPLD +from usrp_mpm.dboard_manager.rh_periphs import DboardClockControl +from usrp_mpm.cores import nijesdcore +from usrp_mpm.dboard_manager.adc_rh import AD9695Rh +from usrp_mpm.dboard_manager.dac_rh import DAC37J82Rh +from usrp_mpm.mpmlog import get_logger +from usrp_mpm.sys_utils.uio import UIO +from usrp_mpm.sys_utils.udev import get_eeprom_paths +from usrp_mpm.bfrfs import BufferFS + + +############################################################################### +# SPI Helpers +############################################################################### + +def create_spidev_iface_lmk(dev_node): + """ + Create a regs iface from a spidev node + """ + return lib.spi.make_spidev_regs_iface( + dev_node, + 1000000, # Speed (Hz) + 0, # SPI mode + 8, # Addr shift + 0, # Data shift + 1<<23, # Read flag + 0 # Write flag + ) + +def create_spidev_iface_cpld(dev_node): + """ + Create a regs iface from a spidev node (CPLD register protocol) + """ + return lib.spi.make_spidev_regs_iface( + dev_node, + 1000000, # Speed (Hz) + 0, # SPI mode + 17, # Addr shift + 0, # Data shift + 1<<16, # Read flag + 0 # Write flag + ) + +def create_spidev_iface_cpld_gain_loader(dev_node): + """ + Create a regs iface from a spidev node (CPLD gain table protocol) + """ + return lib.spi.make_spidev_regs_iface( + dev_node, + 1000000, # Speed (Hz) + 0, # SPI mode + 16, # Addr shift + 4, # Data shift + 0, # Read flag + 1<<3 # Write flag + ) + +def create_spidev_iface_phasedac(dev_node): + """ + Create a regs iface from a spidev node (AD5683) + + The data shift for the SPI interface is defined based on the command + operation defined in the AD5683 datasheet. + Each SPI transaction is 24-bit: [23:20] -> command; [19:0] -> data + The 4 LSBs are all don't cares (Xs), regardless of the DAC's resolution. + Therefore, to simplify DAC writes, we compensate for all the don't care + bits with the data shift parameter here (4), thus 16-bit data field. + Special care must be taken when writing to the control register, + since the 6-bit payload is placed in [19:14] of the SPI transaction, + which is equivalent to bits [15:10] of our 16-bit data field. + For futher details, please refer to the AD5683's datasheet. + """ + return lib.spi.make_spidev_regs_iface( + str(dev_node), + 1000000, # Speed (Hz) + 1, # SPI mode + 20, # Addr shift + 4, # Data shift + 0, # Read flag (phase DAC is write-only) + 0, # Write flag + ) + +def create_spidev_iface_adc(dev_node): + """ + Create a regs iface from a spidev node (AD9695) + """ + return lib.spi.make_spidev_regs_iface( + str(dev_node), + 1000000, # Speed (Hz) + 0, # SPI mode + 8, # Addr shift + 0, # Data shift + 1<<23, # Read flag + 0, # Write flag + ) + +def create_spidev_iface_dac(dev_node): + """ + Create a regs iface from a spidev node (DAC37J82) + """ + return lib.spi.make_spidev_regs_iface( + str(dev_node), + 1000000, # Speed (Hz) + 0, # SPI mode + 16, # Addr shift + 0, # Data shift + 1<<23, # Read flag + 0, # Write flag + ) + + +############################################################################### +# Main dboard control class +############################################################################### + +class Rhodium(DboardManagerBase): + """ + Holds all dboard specific information and methods of the Rhodium dboard + """ + ######################################################################### + # Overridables + # + # See DboardManagerBase for documentation on these fields + ######################################################################### + pids = [0x152] + #file system path to i2c-adapter/mux + base_i2c_adapter = '/sys/class/i2c-adapter' + # Maps the chipselects to the corresponding devices: + spi_chipselect = { + "cpld" : 0, + "cpld_gain_loader" : 0, + "lmk" : 1, + "phase_dac" : 2, + "adc" : 3, + "dac" : 4} + ### End of overridables ################################################# + # Class-specific, but constant settings: + spi_factories = { + "cpld": create_spidev_iface_cpld, + "cpld_gain_loader": create_spidev_iface_cpld_gain_loader, + "lmk": create_spidev_iface_lmk, + "phase_dac": create_spidev_iface_phasedac, + "adc": create_spidev_iface_adc, + "dac": create_spidev_iface_dac + } + # Map I2C channel to slot index + i2c_chan_map = {0: 'i2c-9', 1: 'i2c-10'} + user_eeprom = { + 2: { # RevC + 'label': "e0004000.i2c", + 'offset': 1024, + 'max_size': 32786 - 1024, + 'alignment': 1024, + }, + } + default_master_clock_rate = 245.76e6 + default_time_source = 'internal' + default_current_jesd_rate = 4915.2e6 + + def __init__(self, slot_idx, **kwargs): + super(Rhodium, self).__init__(slot_idx, **kwargs) + self.log = get_logger("Rhodium-{}".format(slot_idx)) + self.log.trace("Initializing Rhodium daughterboard, slot index %d", + self.slot_idx) + self.rev = int(self.device_info['rev']) + self.log.trace("This is a rev: {}".format(chr(65 + self.rev))) + # This is a default ref clock freq, it must be updated before init() is + # called! + self.ref_clock_freq = None + # These will get updated during init() + self.master_clock_rate = None + self.sampling_clock_rate = None + self.current_jesd_rate = None + # Predeclare some attributes to make linter happy: + self.lmk = None + self._port_expander = None + self.cpld = None + # If _init_args is None, it means that init() hasn't yet been called. + self._init_args = None + # Now initialize all peripherals. If that doesn't work, put this class + # into a non-functional state (but don't crash, or we can't talk to it + # any more): + try: + self._init_periphs() + self._periphs_initialized = True + except Exception as ex: + self.log.error("Failed to initialize peripherals: %s", + str(ex)) + self._periphs_initialized = False + + + def _init_periphs(self): + """ + Initialize power and peripherals that don't need user-settings + """ + def _get_i2c_dev(): + " Return the I2C path for this daughterboard " + import pyudev + context = pyudev.Context() + i2c_dev_path = os.path.join( + self.base_i2c_adapter, + self.i2c_chan_map[self.slot_idx] + ) + return pyudev.Devices.from_sys_path(context, i2c_dev_path) + def _init_spi_devices(): + " Returns abstraction layers to all the SPI devices " + self.log.trace("Loading SPI interfaces...") + return { + key: self.spi_factories[key](self._spi_nodes[key]) + for key in self._spi_nodes + } + def _init_dboard_regs(): + " Create a UIO object to talk to dboard regs " + self.log.trace("Getting UIO to talk to dboard regs...") + return UIO( + label="dboard-regs-{}".format(self.slot_idx), + read_only=False + ) + self._port_expander = TCA6408(_get_i2c_dev()) + self._daughterboard_gpio = FPGAtoDbGPIO(self.slot_idx) + self.log.debug("Turning on Module and RF power supplies") + self._power_on() + self._spi_ifaces = _init_spi_devices() + self.log.debug("Loaded SPI interfaces!") + self.cpld = RhCPLD(self._spi_ifaces['cpld'], self.log) + self.log.debug("Loaded CPLD interfaces!") + self.radio_regs = _init_dboard_regs() + self.radio_regs._open() + # Create DAC interface (analog output is disabled). + self.log.trace("Creating DAC control object...") + self.dac = DAC37J82Rh(self.slot_idx, self._spi_ifaces['dac'], self.log) + # Create ADC interface (JESD204B link is powered down). + self.log.trace("Creating ADC control object...") + self.adc = AD9695Rh(self.slot_idx, self._spi_ifaces['adc'], self.log) + self.log.info("Succesfully loaded all peripherals!") + + def _power_on(self): + " Turn on power to daughterboard " + self.log.trace("Powering on slot_idx={}...".format(self.slot_idx)) + self._daughterboard_gpio.set(FPGAtoDbGPIO.DB_POWER_ENABLE, 1) + self._daughterboard_gpio.set(FPGAtoDbGPIO.RF_POWER_ENABLE, 1) + # Check each power good signal + + def _power_off(self): + " Turn off power to daughterboard " + self.log.trace("Powering off slot_idx={}...".format(self.slot_idx)) + self._daughterboard_gpio.set(FPGAtoDbGPIO.DB_POWER_ENABLE, 0) + self._daughterboard_gpio.set(FPGAtoDbGPIO.RF_POWER_ENABLE, 0) + + def _init_user_eeprom(self, eeprom_info): + """ + Reads out user-data EEPROM, and intializes a BufferFS object from that. + """ + self.log.trace("Initializing EEPROM user data...") + eeprom_paths = get_eeprom_paths(eeprom_info.get('label')) + self.log.trace("Found the following EEPROM paths: `{}'".format( + eeprom_paths)) + eeprom_path = eeprom_paths[self.slot_idx] + self.log.trace("Selected EEPROM path: `{}'".format(eeprom_path)) + user_eeprom_offset = eeprom_info.get('offset', 0) + self.log.trace("Selected EEPROM offset: %d", user_eeprom_offset) + user_eeprom_data = open(eeprom_path, 'rb').read()[user_eeprom_offset:] + self.log.trace("Total EEPROM size is: %d bytes", len(user_eeprom_data)) + # FIXME verify EEPROM sectors + return BufferFS( + user_eeprom_data, + max_size=eeprom_info.get('max_size'), + alignment=eeprom_info.get('alignment', 1024), + log=self.log + ), eeprom_path + + def init(self, args): + """ + Execute necessary init dance to bring up dboard + """ + # Sanity checks and input validation: + self.log.info("init() called with args `{}'".format( + ",".join(['{}={}'.format(x, args[x]) for x in args]) + )) + if not self._periphs_initialized: + error_msg = "Cannot run init(), peripherals are not initialized!" + self.log.error(error_msg) + raise RuntimeError(error_msg) + # Check if ref clock freq changed (would require a full init) + ref_clk_freq_changed = False + if 'ref_clk_freq' in args: + new_ref_clock_freq = float(args['ref_clk_freq']) + assert new_ref_clock_freq in (10e6, 20e6, 25e6) + if new_ref_clock_freq != self.ref_clock_freq: + self.ref_clock_freq = new_ref_clock_freq + ref_clk_freq_changed = True + self.log.debug( + "Updating reference clock frequency to {:.02f} MHz!" + .format(self.ref_clock_freq / 1e6) + ) + assert self.ref_clock_freq is not None + # Check if master clock freq changed (would require a full init) + new_master_clock_rate = \ + float(args.get('master_clock_rate', self.default_master_clock_rate)) + assert new_master_clock_rate in (200e6, 245.76e6, 250e6), \ + "Invalid master clock rate: {:.02f} MHz".format(new_master_clock_rate / 1e6) + master_clock_rate_changed = new_master_clock_rate != self.master_clock_rate + if master_clock_rate_changed: + self.master_clock_rate = new_master_clock_rate + self.log.debug("Updating master clock rate to {:.02f} MHz!".format( + self.master_clock_rate / 1e6 + )) + # From the host's perspective (i.e. UHD), master_clock_rate is thought as + # the data rate that the radio NoC block works on (200/245.76/250 MSPS). + # For Rhodium, that rate is different from the RF sampling rate = JESD rate + # (400/491.52/500 MHz). The FPGA has fixed half-band filters that decimate + # and interpolate between the radio block and the JESD core. + # Therefore, the board configuration through MPM relies on the sampling freq., + # so a sampling_clock_rate value is internally set based on the master_clock_rate + # parameter given by the host. + self.sampling_clock_rate = 2 * self.master_clock_rate + self.log.trace("Updating sampling clock rate to {:.02f} MHz!".format( + self.sampling_clock_rate / 1e6 + )) + # Track if we're able to do a "fast reinit", which means there were no + # major changes and can skip all slow initialization steps. + fast_reinit = \ + not bool(args.get("force_reinit", False)) \ + and not master_clock_rate_changed \ + and not ref_clk_freq_changed + if fast_reinit: + self.log.debug("Attempting fast re-init with the following settings: " + "master_clock_rate={} MHz ref_clk_freq={} MHz" + .format(self.master_clock_rate / 1e6, self.ref_clock_freq / 1e6)) + init_result = True + else: + init_result = RhodiumInitManager(self, self._spi_ifaces).init(args) + if init_result: + self._init_args = args + return init_result + + def get_user_eeprom_data(self): + """ + Return a dict of blobs stored in the user data section of the EEPROM. + """ + return { + blob_id: self.eeprom_fs.get_blob(blob_id) + for blob_id in iterkeys(self.eeprom_fs.entries) + } + + def set_user_eeprom_data(self, eeprom_data): + """ + Update the local EEPROM with the data from eeprom_data. + + The actual writing to EEPROM can take some time, and is thus kicked + into a background task. Don't call set_user_eeprom_data() quickly in + succession. Also, while the background task is running, reading the + EEPROM is unavailable and MPM won't be able to reboot until it's + completed. + However, get_user_eeprom_data() will immediately return the correct + data after this method returns. + """ + for blob_id, blob in iteritems(eeprom_data): + self.eeprom_fs.set_blob(blob_id, blob) + self.log.trace("Writing EEPROM info to `{}'".format(self.eeprom_path)) + eeprom_offset = self.user_eeprom[self.rev]['offset'] + def _write_to_eeprom_task(path, offset, data, log): + " Writer task: Actually write to file " + # Note: This can be sped up by only writing sectors that actually + # changed. To do so, this function would need to read out the + # current state of the file, do some kind of diff, and then seek() + # to the different sectors. When very large blobs are being + # written, it doesn't actually help all that much, of course, + # because in that case, we'd anyway be changing most of the EEPROM. + with open(path, 'r+b') as eeprom_file: + log.trace("Seeking forward to `{}'".format(offset)) + eeprom_file.seek(eeprom_offset) + log.trace("Writing a total of {} bytes.".format( + len(self.eeprom_fs.buffer))) + eeprom_file.write(data) + log.trace("EEPROM write complete.") + thread_id = "eeprom_writer_task_{}".format(self.slot_idx) + if any([x.name == thread_id for x in threading.enumerate()]): + # Should this be fatal? + self.log.warn("Another EEPROM writer thread is already active!") + writer_task = threading.Thread( + target=_write_to_eeprom_task, + args=( + self.eeprom_path, + eeprom_offset, + self.eeprom_fs.buffer, + self.log + ), + name=thread_id, + ) + writer_task.start() + # Now return and let the copy finish on its own. The thread will detach + # and MPM this process won't terminate until the thread is complete. + # This does not stop anyone from killing this process (and the thread) + # while the EEPROM write is happening, though. + + + ########################################################################## + # Clocking control APIs + ########################################################################## + + def set_clk_safe_state(self): + """ + Disable all components that could react badly to a sudden change in + clocking. After calling this method, all clocks will be off. Calling + _reinit() will turn them on again. + """ + if self._init_args is None: + # Then we're already in a safe state + return + # Put the ADC and the DAC in a safe state because they receive a LMK's clock. + # The DAC37J82 datasheet only recommends disabling its analog output before + # a clock is provided to the chip. + self.dac.tx_enable(False) + self.adc.power_down_channel(True) + # Clear the Sample Clock enables and place the MMCM in reset. + db_clk_control = DboardClockControl(self.radio_regs, self.log) + db_clk_control.reset_mmcm() + # Place the JESD204b core in reset, mainly to reset QPLL/CPLLs. + jesdcore = nijesdcore.NIJESDCore(self.radio_regs, self.slot_idx, + **RhodiumInitManager.JESD_DEFAULT_ARGS) + jesdcore.reset() + # The reference clock is handled elsewhere since it is a motherboard- + # level clock. + + def _reinit(self, master_clock_rate): + """ + This will re-run init(). We store all the settings in _init_args, so we + will bring the device into the same state that it was before, with the + exception of frequency and gain. Those need to be re-set by UHD in order + not to invalidate the UHD caches. + """ + args = self._init_args + args["master_clock_rate"] = master_clock_rate + args["ref_clk_freq"] = self.ref_clock_freq + # If we add API calls to reset the cals, they need to update + # self._init_args + self.master_clock_rate = None # <= This will force a re-init + self.init(args) + # self.master_clock_rate is now OK again + + def set_master_clock_rate(self, rate): + """ + Set the master clock rate to rate. Note this will trigger a + re-initialization of the entire clocking, unless rate matches the + current master clock rate. + """ + if rate == self.master_clock_rate: + self.log.debug( + "New master clock rate assignment matches previous assignment. " + "Ignoring set_master_clock_rate() command.") + return self.master_clock_rate + self._reinit(rate) + return rate + + def get_master_clock_rate(self): + " Return master clock rate (== sampling rate / 2) " + return self.master_clock_rate + + def update_ref_clock_freq(self, freq, **kwargs): + """ + Call this function if the frequency of the reference clock changes + (the 10, 20, 25 MHz one). + + If this function is called while the device is in an initialized state, + it will also re-trigger the initialization sequence. + + No need to set the device in a safe state because (presumably) the user + has already switched the clock rate externally. All we need to do now + is re-initialize with the new rate. + """ + assert freq in (10e6, 20e6, 25e6), \ + "Invalid ref clock frequency: {}".format(freq) + self.log.trace("Changing ref clock frequency to %f MHz", freq/1e6) + self.ref_clock_freq = freq + if self._init_args is not None: + self._reinit(self.master_clock_rate) + + + ########################################################################## + # Debug + ########################################################################## + + def cpld_peek(self, addr): + """ + Debug for accessing the CPLD via the RPC shell. + """ + self.log.trace("CPLD Signature: 0x{:X}".format(self.cpld.peek(0x00))) + revision_msb = self.cpld.peek16(0x04) + self.log.trace("CPLD Revision: 0x{:X}" + .format(self.cpld.peek16(0x03) | (revision_msb << 16))) + return self.cpld.peek16(addr) + + def cpld_poke(self, addr, data): + """ + Debug for accessing the CPLD via the RPC shell. + """ + self.log.trace("CPLD Signature: 0x{:X}".format(self.cpld.peek16(0x00))) + revision_msb = self.cpld.peek16(0x04) + self.log.trace("CPLD Revision: 0x{:X}" + .format(self.cpld.peek16(0x03) | (revision_msb << 16))) + self.cpld.poke16(addr, data) + return self.cpld.peek16(addr) + + def lmk_peek(self, addr): + """ + Debug for accessing the LMK via the RPC shell. + """ + lmk_regs = self._spi_ifaces['lmk'] + self.log.trace("LMK Chip ID: 0x{:X}".format(lmk_regs.peek8(0x03))) + return lmk_regs.peek8(addr) + + def lmk_poke(self, addr, data): + """ + Debug for accessing the LMK via the RPC shell. + """ + lmk_regs = self._spi_ifaces['lmk'] + self.log.trace("LMK Chip ID: 0x{:X}".format(lmk_regs.peek8(0x03))) + lmk_regs.poke8(addr, data) + return lmk_regs.peek8(addr) + + def pdac_poke(self, addr, data): + """ + Debug for accessing the Phase DAC via the RPC shell. + """ + pdac_regs = self._spi_ifaces['phase_dac'] + pdac_regs.poke16(addr, data) + return + + def adc_peek(self, addr): + """ + Debug for accessing the ADC via the RPC shell. + """ + adc_regs = self._spi_ifaces['adc'] + self.log.trace("ADC Chip ID: 0x{:X}".format(adc_regs.peek8(0x04))) + return adc_regs.peek8(addr) + + def adc_poke(self, addr, data): + """ + Debug for accessing the ADC via the RPC shell + """ + adc_regs = self._spi_ifaces['adc'] + self.log.trace("ADC Chip ID: 0x{:X}".format(adc_regs.peek8(0x04))) + adc_regs.poke8(addr, data) + return adc_regs.peek8(addr) + + def dump_jesd_core(self): + """ + Debug for reading out all JESD core registers via RPC shell + """ + radio_regs = UIO(label="dboard-regs-{}".format(self.slot_idx)) + for i in range(0x2000, 0x2110, 0x10): + print(("0x%04X " % i), end=' ') + for j in range(0, 0x10, 0x4): + print(("%08X" % radio_regs.peek32(i + j)), end=' ') + print("") diff --git a/mpm/python/usrp_mpm/periph_manager/n3xx.py b/mpm/python/usrp_mpm/periph_manager/n3xx.py index 33966a9cb..79dddd898 100644 --- a/mpm/python/usrp_mpm/periph_manager/n3xx.py +++ b/mpm/python/usrp_mpm/periph_manager/n3xx.py @@ -28,6 +28,7 @@ from usrp_mpm.periph_manager.n3xx_periphs import BackpanelGPIO from usrp_mpm.periph_manager.n3xx_periphs import MboardRegsControl from usrp_mpm.dboard_manager.magnesium import Magnesium from usrp_mpm.dboard_manager.eiscat import EISCAT +from usrp_mpm.dboard_manager.rhodium import Rhodium N3XX_DEFAULT_EXT_CLOCK_FREQ = 10e6 N3XX_DEFAULT_CLOCK_SOURCE = 'internal' @@ -41,6 +42,7 @@ N3XX_MONITOR_THREAD_INTERVAL = 1.0 # seconds # Import daughterboard PIDs from their respective classes MG_PID = Magnesium.pids[0] EISCAT_PID = EISCAT.pids[0] +RHODIUM_PID = Rhodium.pids[0] ############################################################################### # Transport managers @@ -101,7 +103,9 @@ class n3xx(ZynqComponents, PeriphManagerBase): # still use the n310.bin image. # We'll leave this here for # debugging purposes. - ('n310', (EISCAT_PID, EISCAT_PID)): 'eiscat', + ('n310', (EISCAT_PID , EISCAT_PID )): 'eiscat', + ('n310', (RHODIUM_PID, RHODIUM_PID)): 'n320', + ('n310', (RHODIUM_PID, )): 'n320', } ######################################################################### |