def set_time_source(self, time_source): " Set a time source " assert time_source in self.get_time_sources() self._time_source = time_source self.mboard_regs_control.set_time_source(time_source, self.get_ref_clock_freq()) if time_source == 'sfp0': # This error is specific to slave and master mode for White Rabbit. # Grand Master mode will require the external or gpsdo sources (not supported). if (time_source == 'sfp0' or time_source == 'sfp1') and \ self.get_clock_source() != 'internal': error_msg = "Time source sfp(0|1) requires the internal clock source!" self.log.error(error_msg) raise RuntimeError(error_msg) if self.updateable_components['fpga']['type'] != 'WX': self.log.error("{} time source requires 'WX' FPGA type" \ .format(time_source)) raise RuntimeError("{} time source requires 'WX' FPGA type" \ .format(time_source)) # Only open UIO to the WR core once we're guaranteed it exists. wr_regs_control = WhiteRabbitRegsControl( self.wr_regs_label, self.log) # Wait for time source to become ready. Only applies to SFP0/1. All other # targets start their PPS immediately. self.log.debug("Waiting for {} timebase to lock..." \ .format(time_source)) if not poll_with_timeout( lambda: wr_regs_control.get_time_lock_status(), 40000, # Try for x ms... this number is set from a few benchtop tests 1000, # Poll every... second! why not? ): self.log.error("{} timebase failed to lock within 40 seconds. Status: 0x{:X}" \ .format(time_source, wr_regs_control.get_time_lock_status())) raise RuntimeError("Failed to lock SFP timebase.")
def pll_sync_trigger(self, clock_ctrl_pps_src): """ Callback for LMK04832 driver to actually trigger the sync. Set PPS source accordingly. """ with self.regs: # Update clock control config register to use the currently relevant # PPS source config = self.peek32(self.MB_CLOCK_CTRL) trigger_config = \ (config & ~MboardRegsControl.CLOCK_CTRL_TRIGGER_PPS_SEL) \ | clock_ctrl_pps_src # trigger sync with appropriate configuration self.poke32(self.MB_CLOCK_CTRL, trigger_config | self.CLOCK_CTRL_PLL_SYNC_TRIGGER) # wait for sync done indication from FPGA # The following value is in ms, it was experimentally picked. pll_sync_timeout = 1500 # ms result = poll_with_timeout(self.is_pll_sync_done, pll_sync_timeout, 10) # de-assert sync trigger signal self.poke32(self.MB_CLOCK_CTRL, trigger_config) if not result: self.log.error("PLL_SYNC_DONE not received within timeout") return result
def __init__(self, i2c_dev): assert i2c_dev is not None self._gpios = SysFSGPIO({'label': 'tca6424', 'device/of_node/name': 'rhodium-lodist-gpio'}, 0x1FFF0F, 0x1FFF00, 0x00A500, i2c_dev) board_rev = self._gpios.get(self.pins.index('BD_REV_0')) + \ self._gpios.get(self.pins.index('BD_REV_1')) << 1 + \ self._gpios.get(self.pins.index('BD_REV_2')) << 2 if board_rev != self.EXPECTED_BOARD_REV: raise RuntimeError('LO distribution board revision did not match: Expected: {0} Actual: {1}'.format(self.EXPECTED_BOARD_REV, board_rev)) self._gpios.set(self.pins.index('P6_8V_EN'), 1) if not poll_with_timeout( lambda: bool(self._gpios.get(self.pins.index('P6_8V_PG'))), self.POWER_ON_TIMEOUT, self.POWER_ON_POLL_INTERVAL): self._gpios.set(self.pins.index('P6_8V_EN'), 0) raise RuntimeError('Power on failure for LO Distribution board') self._gpios.set(self.pins.index('P6_5V_LDO_EN'), 1) self._gpios.set(self.pins.index('P3_3V_RF_EN'), 1)
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 set_voltage_level(self, port, level): """ Change voltage level of a port. This is how EN_<port>, EN_<port>_2V5 and EN_<port>_3V3 are set according to level:: level EN_<port> EN_<port>_2V5 EN_<port>_3V3 off 0 0 0 1V8 1 0 0 2V5 1 1 0 3V3 1 0 1 If level is set to anything other than off this method waits for <port>_PG to go high. Waiting stops as soon as <port>_PG goes high or a timeout of 1s occurs. Note: All pins are set to zero first before the new level is applied. :param port: port to change power level for :param level: new power level :raises RuntimeError: power good pin did not go high """ port = self._normalize_port_name(port) level = level.upper() assert port in self.DIO_PORTS assert level in self.DIO_VOLTAGE_LEVELS port_control = self.port_control[port] self._current_voltage_level[port] = level port_control.enable.set(0) port_control.en_2v5.set(0) port_control.en_3v3.set(0) if level == self.DIO_VOLTAGE_LEVELS[2]: port_control.en_2v5.set(1) elif level == self.DIO_VOLTAGE_LEVELS[3]: port_control.en_3v3.set(1) # wait for <port>_PG to go high if not level == self.DIO_VOLTAGE_LEVELS[0]: # off port_control.enable.set(1) if not poll_with_timeout( lambda: port_control.power_good.get() == 1, 1000, 10): raise RuntimeError( "Power good pin did not go high after power up")
def run_sync(self, measurement_only=False): """ Perform the synchronization algorithm. Successful completion of this function means the clock output was synchronized to the reference. - Set RTC and RSP values in synchronization core - Run offset measurements - Calcuate LMK shift and phase DAC values from offsets - Check it all worked """ # To access registers, use self.peek32() and self.poke32(). It already contains # the offset at this point (see __init__), so self.peek32(0x0000) should read the # first offset if you kept your reg offset at 0 in your netlist self.log.info("Running clock synchronization...") self.log.trace("Using reference clock frequency: {} MHz".format( self.ref_clk_freq / 1e6)) self.log.trace("Using master clock frequency: {} MHz".format( self.radio_clk_freq / 1e6)) # Reset and disable TDC, and enable re-runs. Confirm the core is in # reset and PPS is cleared. Do not disable the PPS crossing. self.poke32(self.TDC_CONTROL, 0x0121) reset_status = self.peek32(self.TDC_STATUS) & 0xFF if reset_status != 0x01: self.log.error( "TDC Failed to Reset! Status: 0x{:x}".format(reset_status)) raise RuntimeError("TDC Failed to reset.") # Set the RSP and RTC values based on the Radio Clock and Reference Clock # configurations. Registers are packed [27:16] = high time, [11:0] = period. def combine_period_hi_time(period, hi_time): """ Registers are packed [27:16] = high time, [11:0] = period. """ assert hi_time <= 0xFFF and period <= 0xFFF return (hi_time << 16) | period rsp_ctrl_word = combine_period_hi_time( *rsp_table(self.ref_clk_freq, self.radio_clk_freq)) rtc_ctrl_word = combine_period_hi_time(*rtc_table(self.radio_clk_freq)) self.log.trace( "Setting RSP control word to: 0x{:08X}".format(rsp_ctrl_word)) self.log.trace( "Setting RTC control word to: 0x{:08X}".format(rtc_ctrl_word)) self.poke32(self.RSP_PERIOD_CONTROL, rsp_ctrl_word) self.poke32(self.RTC_PERIOD_CONTROL, rtc_ctrl_word) # Take the core out of reset, then check the reset done bit cleared. self.poke32(self.TDC_CONTROL, 0x2) reset_status = self.peek32(self.TDC_STATUS) & 0xFF if reset_status != 0x00: self.log.error( "TDC Reset Failed to Clear! " \ "Check that your clocks are toggling. Status: 0x{:x}".format( reset_status )) raise RuntimeError("TDC Reset Failed.") self.log.trace("Enabling the TDC") # Enable the TDC. # As long as PPS is actually a PPS, this doesn't have to happen "synchronously" # across all devices. self.poke32(self.TDC_CONTROL, 0x10) # Since a PPS rising edge comes once per second, we need to wait # slightly longer than a second (worst-case) to confirm the TDC # received a PPS. if not poll_with_timeout( lambda: bool(self.peek32(self.TDC_STATUS) == 0x10), 1100, # Try for 1.1 seconds 100, # Poll every 100 ms ): # Try one last time, just in case there is weirdness with the polling loop. if self.peek32(self.TDC_STATUS) != 0x10: self.log.error("Failed to capture PPS within 1.1 seconds. " \ "TDC_STATUS: 0x{:X}".format(self.peek32(self.TDC_STATUS))) raise RuntimeError("Failed to capture PPS.") self.log.trace("PPS Captured!") measure_offset = lambda: self.read_tdc_meas( 1.0 / self.meas_clk_freq, 1.0 / self.ref_clk_freq, 1.0 / self. radio_clk_freq) # Retrieve the first measurement, but throw it away since it won't align with # all the re-run measurements. self.log.trace("Throwing away first TDC measurement...") measure_offset() # Now, read off 512 measurements and take the mean of them. num_meas = 256 self.log.trace( "Reading {} TDC measurements from device...".format(num_meas)) current_value = mean([measure_offset() for _ in range(num_meas)]) self.log.trace("TDC measurements collected.") # The high and low bounds for this are set programmatically based on the # Reference and Sample Frequencies and the TDC structure. The bounds are: # Low = T_refclk + T_sampleclk*(3) # High = T_refclk + T_sampleclk*(4) # For slop, we add in another T_sampleclk on either side. low_bound = 1.0 / self.ref_clk_freq + (1.0 / self.radio_clk_freq) * 2 high_bound = 1.0 / self.ref_clk_freq + (1.0 / self.radio_clk_freq) * 5 if (current_value < low_bound) or (current_value > high_bound): self.log.error("Clock synchronizer measured a " "current value of {:.3f} ns. " \ "Range is [{:.3f},{:.3f}] ns".format( current_value*1e9, low_bound*1e9, high_bound*1e9)) raise RuntimeError("TDC measurement out of range! " "Current value: {:.3f} ns.".format( current_value * 1e9)) # TEMP CODE for homogeneous rate sync only! Heterogeneous rate sync requires an # identical target value for all devices. target = 1.0 / self.ref_clk_freq + (1.0 / self.radio_clk_freq) * 3.5 # 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-12, 1: 69.0e-12}[self.slot_idx] self.target_values = [ target + trace_delay_offset, ] # Run the initial value through the oracle to determine the adjustments to make. coarse_steps_required, dac_word_delta, distance_to_target = self.oracle( self.target_values, current_value, self.lmk_vco_freq, self.fine_delay_step) # Check the calculated distance_to_target value. It should be less than # +/- 1 radio_clk_freq period. The boundary values are set using the same # logic as the high and low bound checks above on the current_value. if abs(distance_to_target) > 1.0 / self.radio_clk_freq: self.log.error("Clock synchronizer measured a " "distance to target of {:.3f} ns. " \ "Range is [{:.3f},{:.3f}] ns".format( distance_to_target*1e9, -1.0/self.radio_clk_freq*1e9, 1.0/self.radio_clk_freq*1e9)) raise RuntimeError( "TDC measured distance to target is out of range! " "Current value: {:.3f} ns.".format(distance_to_target * 1e9)) if not measurement_only: self.log.trace("Applying calculated shifts...") # Coarse shift with the LMK. self.lmk.lmk_shift(coarse_steps_required) self.log.trace("LMK Shift Complete!") # Fine shift with the DAC, then give it time to take effect. self.write_dac_word(self.current_phase_dac_word + dac_word_delta, 0.5) if not self.lmk.check_plls_locked(): raise RuntimeError( "LMK PLLs lost lock during clock synchronization!") # After shifting the clocks, we enable the PPS crossing from the # RefClk into the SampleClk domain. We never explicitly turn off the # crossing from this point forward, even if we re-run this routine. self.poke32(self.TDC_CONTROL, 0x1000) return distance_to_target
def set_sync_source(self, args): """ Selects reference clock and PPS sources. Unconditionally re-applies the time source to ensure continuity between the reference clock and time rates. """ clock_source = args.get('clock_source', self._clock_source) assert clock_source in self.get_clock_sources() time_source = args.get('time_source', self._time_source) assert time_source in self.get_time_sources() if (clock_source == self._clock_source) and (time_source == self._time_source): # Nothing change no need to do anything self.log.trace("New sync source assignment matches" "previous assignment. Ignoring update command.") return assert (clock_source, time_source) in self.valid_sync_sources # Start setting sync source self.log.debug("Setting clock source to `{}'".format(clock_source)) # Place the DB clocks in a safe state to allow reference clock # transitions. This leaves all the DB clocks OFF. for slot, dboard in enumerate(self.dboards): if hasattr(dboard, 'set_clk_safe_state'): self.log.trace( "Setting dboard %d components to safe clocking state...", slot) dboard.set_clk_safe_state() # Disable the Ref Clock in the FPGA before throwing the external switches. self.mboard_regs_control.enable_ref_clk(False) # Set the external switches to bring in the new source. if clock_source == 'internal': self._gpios.set("CLK-MAINSEL-EX_B") self._gpios.set("CLK-MAINSEL-25MHz") self._gpios.reset("CLK-MAINSEL-GPS") elif clock_source == 'gpsdo': self._gpios.set("CLK-MAINSEL-EX_B") self._gpios.reset("CLK-MAINSEL-25MHz") self._gpios.set("CLK-MAINSEL-GPS") else: # external self._gpios.reset("CLK-MAINSEL-EX_B") self._gpios.reset("CLK-MAINSEL-GPS") # SKY13350 needs to be in known state self._gpios.set("CLK-MAINSEL-25MHz") self._clock_source = clock_source self.log.debug("Reference clock source is: {}" \ .format(self._clock_source)) self.log.debug("Reference clock frequency is: {} MHz" \ .format(self.get_ref_clock_freq()/1e6)) # Enable the Ref Clock in the FPGA after giving it a chance to # settle. The settling time is a guess. time.sleep(0.100) self.mboard_regs_control.enable_ref_clk(True) self.log.debug("Setting time source to `{}'".format(time_source)) self._time_source = time_source ref_clk_freq = self.get_ref_clock_freq() self.mboard_regs_control.set_time_source(time_source, ref_clk_freq) if time_source == 'sfp0': # This error is specific to slave and master mode for White Rabbit. # Grand Master mode will require the external or gpsdo # sources (not supported). if time_source in ('sfp0', 'sfp1') \ and self.get_clock_source() != 'internal': error_msg = "Time source {} requires `internal` clock source!".format( time_source) self.log.error(error_msg) raise RuntimeError(error_msg) sfp_time_source_images = ('WX', ) if self.updateable_components['fpga'][ 'type'] not in sfp_time_source_images: self.log.error("{} time source requires FPGA types {}" \ .format(time_source, sfp_time_source_images)) raise RuntimeError("{} time source requires FPGA types {}" \ .format(time_source, sfp_time_source_images)) # Only open UIO to the WR core once we're guaranteed it exists. wr_regs_control = WhiteRabbitRegsControl(self.wr_regs_label, self.log) # Wait for time source to become ready. Only applies to SFP0/1. All other # targets start their PPS immediately. self.log.debug("Waiting for {} timebase to lock..." \ .format(time_source)) if not poll_with_timeout( lambda: wr_regs_control.get_time_lock_status(), 40000, # Try for x ms... this number is set from a few benchtop tests 1000, # Poll every... second! why not? ): self.log.error("{} timebase failed to lock within 40 seconds. Status: 0x{:X}" \ .format(time_source, wr_regs_control.get_time_lock_status())) raise RuntimeError("Failed to lock SFP timebase.") # Update the DB with the correct Ref Clock frequency and force a re-init. for slot, dboard in enumerate(self.dboards): self.log.trace( "Updating reference clock on dboard %d to %f MHz...", slot, ref_clk_freq / 1e6) dboard.update_ref_clock_freq(ref_clk_freq, time_source=time_source, clock_source=clock_source, skip_rfic=args.get('skip_rfic', None))
def configure(self, force=False): """ Perform a soft reset on the TDC, then configure the TDC registers in the FPGA based on the reference and master clock rates. Enable the TDC and wait for the next PPS to arrive. Will throw on error. Otherwise returns nothing. """ if self.configured: if not force: self.log.debug("TDC is already configured. " \ "Skipping configuration sequence!") return None else: # Apparently force is specified... this could lead to some strange # TDC behavior, but we do it anyway. self.log.debug("TDC is already configured, but Force is specified..." \ "reconfiguring the TDC anyway!") self.log.debug("Configuring the TDC...") self.log.trace("Using reference clock frequency: {:.3f} MHz" \ .format(self.ref_clk_freq/1e6)) self.log.trace("Using master clock frequency: {:.3f} MHz" \ .format(self.radio_clk_freq/1e6)) meas_clk_ref_freq = 166.666666666667e6 if self.tdc_rev == 1: self.meas_clk_freq = meas_clk_ref_freq * 5.5 / 1 / 5.375 else: self.meas_clk_freq = meas_clk_ref_freq * 21.875 / 3 / 6.125 self.log.trace("Using measurement clock frequency: {:.10f} MHz" \ .format(self.meas_clk_freq/1e6)) self.configured = False # Reset and disable TDC, clear PPS crossing, and enable re-runs. Confirm the # core is in reset and PPS is cleared. self.poke32(self.TDC_CONTROL, 0x2121) reset_status = self.peek32(self.TDC_STATUS) & 0xFF if reset_status != 0x01: self.log.error("TDC Failed to Reset! Check your clocks! Status: 0x{:x}" \ .format(reset_status)) raise RuntimeError("TDC Failed to reset.") def get_pulse_setup(clk_freq, pulser, compat_mode=False): """ Set the pulser divide values based on the given clock rates. Returns register value required to create the desired pulses. """ # Compatibility mode runs at 40 kHz. This only supports these clock rates: # 10, 20, 25, 125, 122.88, and 153.6 MHz. Any other rates are expected # to use the TDC 2.0 and later. if compat_mode: pulse_rate = 40e3 # The RP always runs at 1 MHz since all of our reference clock rates are # multiples of 1. elif pulser == "rp": pulse_rate = 1.00e6 # The SP either runs at 1.0, 1.2288, or 1.25 MHz. If the clock rate doesn't # divide nicely into 1 MHz, we can use the alternative rates. elif pulser == "sp": pulse_rate = 1.00e6 if math.modf(clk_freq / pulse_rate)[0] > 0: pulse_rate = 1.2288e6 if math.modf(clk_freq / pulse_rate)[0] > 0: pulse_rate = 1.25e6 # The Restart-pulser must run at the GCD of the RP and SP rates, not the # Reference Clock and Radio Clock rates! For ease of implementation, # run the pulser at a fixed value. elif (pulser == "repulse-1") or (pulser == "repulse-2"): pulse_rate = 1.6e3 # 156.25 MHz: this value needs to be 400 Hz, which is insanely # slow and doubles the measurement time... so only use it for this # specific case. if clk_freq == 156.25e6: pulse_rate = 400 # The RP-t and SP-t pulsers always run at 10 kHz, which is the GCD of all # supported clock rates. elif (pulser == "rpt") or (pulser == "spt"): pulse_rate = 10e3 else: pulse_rate = 10e3 # Check that the chosen pulse_rate divides evenly in the clk_freq. if math.modf(clk_freq / pulse_rate)[0] > 0: self.log.error("TDC Setup Failure: Pulse rate setup failed for {}!" \ .format(pulser)) raise RuntimeError("TDC Failed to Initialize. Check your clock rates " \ "for compatibility!") period = int(clk_freq / pulse_rate) hi_time = int(math.floor(period / 2)) if pulser == "repulse-1": # The re-pulse is broken into two registers: # -1 is the period and -2 is the high time + compatibility bit assert period <= 0xFFFFFF # 24 bits return period & 0xFFFFFF elif pulser == "repulse-2": assert hi_time <= 0x7FFFFF # 23 bits return (hi_time & 0x7FFFFF) # All the other registers are packed [30:16] = high time, [15:0] = period. # hi_time is period/2 so we only use 15 bits. assert hi_time <= 0x7FFF and period <= 0xFFFF return (hi_time << 16) | period compat_mode = self.tdc_rev < 2 if compat_mode: self.log.warning("Running TDC in Compatibility Mode for v1.0!") repulse_ctrl_word1 = get_pulse_setup(self.ref_clk_freq, "repulse-1") repulse_ctrl_word2 = get_pulse_setup(self.ref_clk_freq, "repulse-2") rp_ctrl_word = get_pulse_setup(self.ref_clk_freq, "rp", compat_mode) sp_ctrl_word = get_pulse_setup(self.radio_clk_freq, "sp", compat_mode) rpt_ctrl_word = get_pulse_setup(self.ref_clk_freq, "rpt") spt_ctrl_word = get_pulse_setup(self.radio_clk_freq, "spt") self.log.trace("Setting RePulse-1 control word to: 0x{:08X}".format( repulse_ctrl_word1)) self.log.trace("Setting RePulse-2 control word to: 0x{:08X}".format( repulse_ctrl_word2)) self.log.trace( "Setting RP control word to: 0x{:08X}".format(rp_ctrl_word)) self.log.trace( "Setting SP control word to: 0x{:08X}".format(sp_ctrl_word)) self.log.trace( "Setting RPT control word to: 0x{:08X}".format(rpt_ctrl_word)) self.log.trace( "Setting SPT control word to: 0x{:08X}".format(spt_ctrl_word)) self.poke32(self.REPULSE_PERIOD_CONTROL_1, repulse_ctrl_word1) self.poke32(self.REPULSE_PERIOD_CONTROL_2, repulse_ctrl_word2) self.poke32(self.RP_PERIOD_CONTROL, rp_ctrl_word) self.poke32(self.SP_PERIOD_CONTROL, sp_ctrl_word) self.poke32(self.RPT_PERIOD_CONTROL, rpt_ctrl_word) self.poke32(self.SPT_PERIOD_CONTROL, spt_ctrl_word) # Take the core out of reset, then check the reset done bit cleared. self.poke32(self.TDC_CONTROL, 0x2) reset_status = self.peek32(self.TDC_STATUS) & 0xFF if reset_status != 0x00: self.log.error( "TDC Reset Failed to Clear! " \ "Check that your clocks are toggling. Status: 0x{:x}" .format(reset_status) ) raise RuntimeError("TDC Reset Failed.") # Set the PPS crossing delay from the SP-t rising edge to the PPS pulse # in the Radio Clock domain. # delay = [19..16], update = 20 reg_val = (self.pps_out_pipe_var_delay & 0xF) << 16 | 0b1 << 20 self.poke32(self.TDC_CONTROL, reg_val) # Enable the TDC to capture the PPS. As long as PPS is actually a PPS, this # doesn't have to happen "synchronously" across all devices. Each device can # choose a different PPS and still be aligned. self.log.trace("Enabling the TDC...") self.poke32(self.TDC_CONTROL, 0x10) # Since a PPS rising edge comes once per second, we only need to wait # slightly longer than a second (worst-case) to confirm the TDC # received a PPS. if not poll_with_timeout( lambda: bool(self.peek32(self.TDC_STATUS) == 0x10), 1100, # Try for 1.1 seconds 100, # Poll every 100 ms ): # Try one last time, just in case there is weirdness with the polling loop. if self.peek32(self.TDC_STATUS) != 0x10: self.log.error("Failed to capture PPS within 1.1 seconds. " \ "TDC_STATUS: 0x{:X}".format(self.peek32(self.TDC_STATUS))) raise RuntimeError("Failed to capture PPS.") self.log.trace("PPS Captured!") self.configured = True
def configure(self, force=False): """ Perform a soft reset on the TDC, then configure the TDC registers in the FPGA based on the reference and master clock rates. Enable the TDC and wait for the next PPS to arrive. Will throw on error. Otherwise returns nothing. """ if self.configured: if not force: self.log.debug("TDC is already configured. " \ "Skipping configuration sequence!") return None else: # Apparently force is specified... this could lead to some strange # TDC behavior, but we do it anyway. self.log.debug("TDC is already configured, but Force is specified..." \ "reconfiguring the TDC anyway!") self.log.debug("Configuring the TDC...") self.log.trace("Using reference clock frequency: {:.3f} MHz" \ .format(self.ref_clk_freq/1e6)) self.log.trace("Using master clock frequency: {:.3f} MHz" \ .format(self.radio_clk_freq/1e6)) meas_clk_ref_freq = 166.666666666667e6 if self.tdc_rev == 1: self.meas_clk_freq = meas_clk_ref_freq*5.5/1/5.375 else: self.meas_clk_freq = meas_clk_ref_freq*21.875/3/6.125 self.log.trace("Using measurement clock frequency: {:.10f} MHz" \ .format(self.meas_clk_freq/1e6)) self.configured = False # Reset and disable TDC, clear PPS crossing, and enable re-runs. Confirm the # core is in reset and PPS is cleared. self.poke32(self.TDC_CONTROL, 0x2121) reset_status = self.peek32(self.TDC_STATUS) & 0xFF if reset_status != 0x01: self.log.error("TDC Failed to Reset! Check your clocks! Status: 0x{:x}" \ .format(reset_status)) raise RuntimeError("TDC Failed to reset.") def find_rate(clk_freq, rates): """ Go through the rates list in sequential order, checking each rate for even division into our clk_freq. Return the first one we find. """ for rate in rates: if math.modf(clk_freq/rate)[0] == 0: return rate self.log.error("TDC Setup Failure: Pulse rate setup failed for {:.4f} MHz!" \ .format(clk_freq/1e6)) raise RuntimeError("TDC Failed to Initialize. No pulse rate found!") def get_pulse_setup(clk_freq, pulser, compat_mode=False): """ Set the pulser divide values based on the given clock rates. Returns register value required to create the desired pulses. """ # Compatibility mode runs at 40 kHz. This only supports these clock rates: # 10, 20, 25, 125, 122.88, and 153.6 MHz. Any other rates are expected # to use the TDC 2.0 and later. if compat_mode: pulse_rate = find_rate(clk_freq, [40e3]) elif (pulser == "rp") or (pulser == "sp"): pulse_rate = find_rate(clk_freq, self.SUPPORTED_PULSE_RATES) # The RP-t and SP-t pulsers always run at 10 kHz, which is the GCD of all # supported clock rates. elif (pulser == "rpt") or (pulser == "spt"): pulse_rate = find_rate(clk_freq, [10e3]) else: self.log.error("TDC Setup Failure: Unrecognized pulse name given: {}" \ .format(pulser)) raise RuntimeError("TDC Failed to Initialize. " "Unrecognized pulse name given!") period = int(clk_freq/pulse_rate) hi_time = int(math.floor(period/2)) # All registers are packed [30:16] = high time, [15:0] = period. # hi_time is period/2 so we only use 15 bits. assert hi_time <= 0x7FFF and period <= 0xFFFF ctrl_word = (hi_time << 16) | period return pulse_rate, ctrl_word def get_restart_pulse_setup(rp_rate, sp_rate): """ Set the restart pulser divide values based on the repeating pulse rates. Returns register value required to create the desired pulses. """ # The Restart-pulser must run at the GCD of the RP and SP rates, not the # Reference Clock and Radio Clock rates! pulse_rate = find_rate(self.ref_clk_freq, [gcd(rp_rate, sp_rate)]) period = int(self.ref_clk_freq/pulse_rate) hi_time = int(math.floor(period/2)) # The re-pulse is broken into two registers: # -1 is the period and -2 is the high time assert period <= 0xFFFFFF # 24 bits assert hi_time <= 0x7FFFFF # 23 bits return pulse_rate, (period & 0xFFFFFF), (hi_time & 0x7FFFFF) compat_mode = self.tdc_rev < 2 if compat_mode: self.log.warning("Running TDC in Compatibility Mode for v1.0!") rp_rate, rp_ctrl_word = get_pulse_setup(self.ref_clk_freq, "rp", compat_mode) sp_rate, sp_ctrl_word = get_pulse_setup(self.radio_clk_freq,"sp", compat_mode) rpt_rate,rpt_ctrl_word = get_pulse_setup(self.ref_clk_freq, "rpt") spt_rate,spt_ctrl_word = get_pulse_setup(self.radio_clk_freq,"spt") rep_rate, repulse_ctrl_word1, repulse_ctrl_word2 = \ get_restart_pulse_setup(rp_rate, sp_rate) self.log.trace("Using RP pulse rate: {} MHz".format(rp_rate/1e6)) self.log.trace("Using SP pulse rate: {} MHz".format(sp_rate/1e6)) self.log.trace("Using RPT pulse rate: {} MHz".format(rpt_rate/1e6)) self.log.trace("Using SPT pulse rate: {} MHz".format(spt_rate/1e6)) self.log.trace("Using Restart pulse rate: {} MHz".format(rep_rate/1e6)) self.log.trace("Setting RP control word to: 0x{:08X}".format(rp_ctrl_word)) self.log.trace("Setting SP control word to: 0x{:08X}".format(sp_ctrl_word)) self.log.trace("Setting RPT control word to: 0x{:08X}".format(rpt_ctrl_word)) self.log.trace("Setting SPT control word to: 0x{:08X}".format(spt_ctrl_word)) self.log.trace("Setting RePulse-1 control word to: 0x{:08X}" \ .format(repulse_ctrl_word1)) self.log.trace("Setting RePulse-2 control word to: 0x{:08X}" \ .format(repulse_ctrl_word2)) self.poke32(self.REPULSE_PERIOD_CONTROL_1, repulse_ctrl_word1) self.poke32(self.REPULSE_PERIOD_CONTROL_2, repulse_ctrl_word2) self.poke32(self.RP_PERIOD_CONTROL, rp_ctrl_word) self.poke32(self.SP_PERIOD_CONTROL, sp_ctrl_word) self.poke32(self.RPT_PERIOD_CONTROL, rpt_ctrl_word) self.poke32(self.SPT_PERIOD_CONTROL, spt_ctrl_word) # Take the core out of reset, then check the reset done bit cleared. self.poke32(self.TDC_CONTROL, 0x2) reset_status = self.peek32(self.TDC_STATUS) & 0xFF if reset_status != 0x00: self.log.error( "TDC Reset Failed to Clear! " \ "Check that your clocks are toggling. Status: 0x{:x}" .format(reset_status) ) raise RuntimeError("TDC Reset Failed.") # Set the PPS crossing delay from the SP-t rising edge to the PPS pulse # in the Radio Clock domain. # delay = [19..16], update = 20 reg_val = (self.pps_out_pipe_var_delay & 0xF) << 16 | 0b1 << 20 self.poke32(self.TDC_CONTROL, reg_val) # Set the pulser enable delay from the RePulse enable to the RP enable. # Actual delay is +1 whatever is written here. Default is 1. # delay = [27..24], update = 28 reg_val = (self.pps_in_pipe_dynamic_delay & 0xF) << 24 | 0b1 << 28 self.poke32(self.TDC_CONTROL, reg_val) # Enable the TDC to capture the PPS. As long as PPS is actually a PPS, this # doesn't have to happen "synchronously" across all devices. Each device can # choose a different PPS and still be aligned. self.log.trace("Enabling the TDC...") self.poke32(self.TDC_CONTROL, 0x10) # Since a PPS rising edge comes once per second, we only need to wait # slightly longer than a second (worst-case) to confirm the TDC # received a PPS. if not poll_with_timeout( lambda: bool(self.peek32(self.TDC_STATUS) == 0x10), 1100, # Try for 1.1 seconds 100, # Poll every 100 ms ): # Try one last time, just in case there is weirdness with the polling loop. if self.peek32(self.TDC_STATUS) != 0x10: self.log.error("Failed to capture PPS within 1.1 seconds. " \ "TDC_STATUS: 0x{:X}".format(self.peek32(self.TDC_STATUS))) raise RuntimeError("Failed to capture PPS.") self.log.trace("PPS Captured!") self.configured = True
def set_sync_source(self, args): """ Selects reference clock and PPS sources. Unconditionally re-applies the time source to ensure continuity between the reference clock and time rates. """ clock_source = args.get('clock_source', self._clock_source) assert clock_source in self.get_clock_sources() time_source = args.get('time_source', self._time_source) assert time_source in self.get_time_sources() if (clock_source == self._clock_source) and (time_source == self._time_source): # Nothing change no need to do anything self.log.trace("New sync source assignment matches" "previous assignment. Ignoring update command.") return assert (clock_source, time_source) in self.valid_sync_sources # Start setting sync source self.log.debug("Setting clock source to `{}'".format(clock_source)) # Place the DB clocks in a safe state to allow reference clock # transitions. This leaves all the DB clocks OFF. for slot, dboard in enumerate(self.dboards): if hasattr(dboard, 'set_clk_safe_state'): self.log.trace( "Setting dboard %d components to safe clocking state...", slot) dboard.set_clk_safe_state() # Disable the Ref Clock in the FPGA before throwing the external switches. self.mboard_regs_control.enable_ref_clk(False) # Set the external switches to bring in the new source. if clock_source == 'internal': self._gpios.set("CLK-MAINSEL-EX_B") self._gpios.set("CLK-MAINSEL-25MHz") self._gpios.reset("CLK-MAINSEL-GPS") elif clock_source == 'gpsdo': self._gpios.set("CLK-MAINSEL-EX_B") self._gpios.reset("CLK-MAINSEL-25MHz") self._gpios.set("CLK-MAINSEL-GPS") else: # external self._gpios.reset("CLK-MAINSEL-EX_B") self._gpios.reset("CLK-MAINSEL-GPS") # SKY13350 needs to be in known state self._gpios.set("CLK-MAINSEL-25MHz") self._clock_source = clock_source self.log.debug("Reference clock source is: {}" \ .format(self._clock_source)) self.log.debug("Reference clock frequency is: {} MHz" \ .format(self.get_ref_clock_freq()/1e6)) # Enable the Ref Clock in the FPGA after giving it a chance to # settle. The settling time is a guess. time.sleep(0.100) self.mboard_regs_control.enable_ref_clk(True) self.log.debug("Setting time source to `{}'".format(time_source)) self._time_source = time_source ref_clk_freq = self.get_ref_clock_freq() self.mboard_regs_control.set_time_source(time_source, ref_clk_freq) if time_source == 'sfp0': # This error is specific to slave and master mode for White Rabbit. # Grand Master mode will require the external or gpsdo # sources (not supported). if time_source in ('sfp0', 'sfp1') \ and self.get_clock_source() != 'internal': error_msg = "Time source {} requires `internal` clock source!".format( time_source) self.log.error(error_msg) raise RuntimeError(error_msg) sfp_time_source_images = ('WX',) if self.updateable_components['fpga']['type'] not in sfp_time_source_images: self.log.error("{} time source requires FPGA types {}" \ .format(time_source, sfp_time_source_images)) raise RuntimeError("{} time source requires FPGA types {}" \ .format(time_source, sfp_time_source_images)) # Only open UIO to the WR core once we're guaranteed it exists. wr_regs_control = WhiteRabbitRegsControl( self.wr_regs_label, self.log) # Wait for time source to become ready. Only applies to SFP0/1. All other # targets start their PPS immediately. self.log.debug("Waiting for {} timebase to lock..." \ .format(time_source)) if not poll_with_timeout( lambda: wr_regs_control.get_time_lock_status(), 40000, # Try for x ms... this number is set from a few benchtop tests 1000, # Poll every... second! why not? ): self.log.error("{} timebase failed to lock within 40 seconds. Status: 0x{:X}" \ .format(time_source, wr_regs_control.get_time_lock_status())) raise RuntimeError("Failed to lock SFP timebase.") # Update the DB with the correct Ref Clock frequency and force a re-init. for slot, dboard in enumerate(self.dboards): self.log.trace( "Updating reference clock on dboard %d to %f MHz...", slot, ref_clk_freq/1e6 ) dboard.update_ref_clock_freq( ref_clk_freq, time_source=time_source, clock_source=clock_source, skip_rfic=args.get('skip_rfic', None) )
def configure(self, force=False): """ Perform a soft reset on the TDC, then configure the TDC registers in the FPGA based on the reference and master clock rates. Enable the TDC and wait for the next PPS to arrive. Will throw on error. Otherwise returns nothing. """ if self.configured: if not force: self.log.debug("TDC is already configured. " \ "Skipping configuration sequence!") return None else: # Apparently force is specified... this could lead to some strange # TDC behavior, but we do it anyway. self.log.debug("TDC is already configured, but Force is specified..." \ "reconfiguring the TDC anyway!") self.log.debug("Configuring the TDC...") self.log.trace("Using reference clock frequency: {:.3f} MHz" \ .format(self.ref_clk_freq/1e6)) self.log.trace("Using master clock frequency: {:.3f} MHz" \ .format(self.radio_clk_freq/1e6)) meas_clk_ref_freq = 166.666666666667e6 if self.tdc_rev == 1: self.meas_clk_freq = meas_clk_ref_freq * 5.5 / 1 / 5.375 else: self.meas_clk_freq = meas_clk_ref_freq * 21.875 / 3 / 6.125 self.log.trace("Using measurement clock frequency: {:.10f} MHz" \ .format(self.meas_clk_freq/1e6)) self.configured = False # Reset and disable TDC, clear PPS crossing, and enable re-runs. Confirm the # core is in reset and PPS is cleared. self.poke32(self.TDC_CONTROL, 0x2121) reset_status = self.peek32(self.TDC_STATUS) & 0xFF if reset_status != 0x01: self.log.error("TDC Failed to Reset! Check your clocks! Status: 0x{:x}" \ .format(reset_status)) raise RuntimeError("TDC Failed to reset.") def find_rate(clk_freq, rates): """ Go through the rates list in sequential order, checking each rate for even division into our clk_freq. Return the first one we find. """ for rate in rates: if math.modf(clk_freq / rate)[0] == 0: return rate self.log.error("TDC Setup Failure: Pulse rate setup failed for {:.4f} MHz!" \ .format(clk_freq/1e6)) raise RuntimeError( "TDC Failed to Initialize. No pulse rate found!") def get_pulse_setup(clk_freq, pulser, compat_mode=False): """ Set the pulser divide values based on the given clock rates. Returns register value required to create the desired pulses. """ # Compatibility mode runs at 40 kHz. This only supports these clock rates: # 10, 20, 25, 125, 122.88, and 153.6 MHz. Any other rates are expected # to use the TDC 2.0 and later. if compat_mode: pulse_rate = find_rate(clk_freq, [40e3]) elif (pulser == "rp") or (pulser == "sp"): pulse_rate = find_rate(clk_freq, self.SUPPORTED_PULSE_RATES) # The RP-t and SP-t pulsers always run at 10 kHz, which is the GCD of all # supported clock rates. elif (pulser == "rpt") or (pulser == "spt"): pulse_rate = find_rate(clk_freq, [10e3]) else: self.log.error("TDC Setup Failure: Unrecognized pulse name given: {}" \ .format(pulser)) raise RuntimeError("TDC Failed to Initialize. " "Unrecognized pulse name given!") period = int(clk_freq / pulse_rate) hi_time = int(math.floor(period / 2)) # All registers are packed [30:16] = high time, [15:0] = period. # hi_time is period/2 so we only use 15 bits. assert hi_time <= 0x7FFF and period <= 0xFFFF ctrl_word = (hi_time << 16) | period return pulse_rate, ctrl_word def get_restart_pulse_setup(rp_rate, sp_rate): """ Set the restart pulser divide values based on the repeating pulse rates. Returns register value required to create the desired pulses. """ # The Restart-pulser must run at the GCD of the RP and SP rates, not the # Reference Clock and Radio Clock rates! pulse_rate = find_rate(self.ref_clk_freq, [gcd(rp_rate, sp_rate)]) period = int(self.ref_clk_freq / pulse_rate) hi_time = int(math.floor(period / 2)) # The re-pulse is broken into two registers: # -1 is the period and -2 is the high time assert period <= 0xFFFFFF # 24 bits assert hi_time <= 0x7FFFFF # 23 bits return pulse_rate, (period & 0xFFFFFF), (hi_time & 0x7FFFFF) compat_mode = self.tdc_rev < 2 if compat_mode: self.log.warning("Running TDC in Compatibility Mode for v1.0!") rp_rate, rp_ctrl_word = get_pulse_setup(self.ref_clk_freq, "rp", compat_mode) sp_rate, sp_ctrl_word = get_pulse_setup(self.radio_clk_freq, "sp", compat_mode) rpt_rate, rpt_ctrl_word = get_pulse_setup(self.ref_clk_freq, "rpt") spt_rate, spt_ctrl_word = get_pulse_setup(self.radio_clk_freq, "spt") rep_rate, repulse_ctrl_word1, repulse_ctrl_word2 = \ get_restart_pulse_setup(rp_rate, sp_rate) self.log.trace("Using RP pulse rate: {} MHz".format(rp_rate / 1e6)) self.log.trace("Using SP pulse rate: {} MHz".format(sp_rate / 1e6)) self.log.trace("Using RPT pulse rate: {} MHz".format(rpt_rate / 1e6)) self.log.trace("Using SPT pulse rate: {} MHz".format(spt_rate / 1e6)) self.log.trace("Using Restart pulse rate: {} MHz".format(rep_rate / 1e6)) self.log.trace( "Setting RP control word to: 0x{:08X}".format(rp_ctrl_word)) self.log.trace( "Setting SP control word to: 0x{:08X}".format(sp_ctrl_word)) self.log.trace( "Setting RPT control word to: 0x{:08X}".format(rpt_ctrl_word)) self.log.trace( "Setting SPT control word to: 0x{:08X}".format(spt_ctrl_word)) self.log.trace("Setting RePulse-1 control word to: 0x{:08X}" \ .format(repulse_ctrl_word1)) self.log.trace("Setting RePulse-2 control word to: 0x{:08X}" \ .format(repulse_ctrl_word2)) self.poke32(self.REPULSE_PERIOD_CONTROL_1, repulse_ctrl_word1) self.poke32(self.REPULSE_PERIOD_CONTROL_2, repulse_ctrl_word2) self.poke32(self.RP_PERIOD_CONTROL, rp_ctrl_word) self.poke32(self.SP_PERIOD_CONTROL, sp_ctrl_word) self.poke32(self.RPT_PERIOD_CONTROL, rpt_ctrl_word) self.poke32(self.SPT_PERIOD_CONTROL, spt_ctrl_word) # Take the core out of reset, then check the reset done bit cleared. self.poke32(self.TDC_CONTROL, 0x2) reset_status = self.peek32(self.TDC_STATUS) & 0xFF if reset_status != 0x00: self.log.error( "TDC Reset Failed to Clear! " \ "Check that your clocks are toggling. Status: 0x{:x}" .format(reset_status) ) raise RuntimeError("TDC Reset Failed.") # Set the PPS crossing delay from the SP-t rising edge to the PPS pulse # in the Radio Clock domain. # delay = [19..16], update = 20 reg_val = (self.pps_out_pipe_var_delay & 0xF) << 16 | 0b1 << 20 self.poke32(self.TDC_CONTROL, reg_val) # Set the pulser enable delay from the RePulse enable to the RP enable. # Actual delay is +1 whatever is written here. Default is 1. # delay = [27..24], update = 28 reg_val = (self.pps_in_pipe_dynamic_delay & 0xF) << 24 | 0b1 << 28 self.poke32(self.TDC_CONTROL, reg_val) # Enable the TDC to capture the PPS. As long as PPS is actually a PPS, this # doesn't have to happen "synchronously" across all devices. Each device can # choose a different PPS and still be aligned. self.log.trace("Enabling the TDC...") self.poke32(self.TDC_CONTROL, 0x10) # Since a PPS rising edge comes once per second, we only need to wait # slightly longer than a second (worst-case) to confirm the TDC # received a PPS. if not poll_with_timeout( lambda: bool(self.peek32(self.TDC_STATUS) == 0x10), 1100, # Try for 1.1 seconds 100, # Poll every 100 ms ): # Try one last time, just in case there is weirdness with the polling loop. if self.peek32(self.TDC_STATUS) != 0x10: self.log.error("Failed to capture PPS within 1.1 seconds. " \ "TDC_STATUS: 0x{:X}".format(self.peek32(self.TDC_STATUS))) raise RuntimeError("Failed to capture PPS.") self.log.trace("PPS Captured!") self.configured = True