Example #1
0
def main():
    " Dawaj, dawaj! "
    args = parse_args()
    # Initialize logger for downstream components
    mpm.get_main_logger().getChild('main')
    master_core_uio = UIO(label=args.uio_dev, read_only=False)
    master_core_uio.open()
    if args.slave_uio_dev:
        slave_core_uio = UIO(label=args.uio_dev, read_only=False)
        slave_core_uio.open()
    try:
        master_core = AuroraControl(master_core_uio, args.base_addr)
        slave_core = None if args.slave_uio_dev is None else AuroraControl(
            slave_core_uio,
            args.slave_base_addr,
        )
        if args.loopback:
            master_core.reset_core()
            master_core.set_loopback(enable=True)
            return True
        # Run BIST
        if args.test == 'ber':
            print("Performing BER BIST test.")
            master_core.run_ber_loopback_bist(
                args.duration,
                args.rate * 8e6,
                slave_core,
            )
        else:
            print("Performing Latency BIST test.")
            master_core.run_latency_loopback_bist(
                args.duration,
                args.rate * 8e6,
                slave_core,
            )
    except Exception as ex:
        print("Unexpected exception: {}".format(str(ex)))
        return False
    finally:
        master_core_uio.close()
        if args.slave_uio_dev:
            slave_core_uio.close()
Example #2
0
def main():
    " Dawaj, dawaj! "
    args = parse_args()
    # Initialize logger for downstream components
    mpm.get_main_logger().getChild('main')
    master_core_uio = UIO(label=args.uio_dev, read_only=False)
    master_core_uio.open()
    if args.slave_uio_dev:
        slave_core_uio = UIO(label=args.uio_dev, read_only=False)
        slave_core_uio.open()
    try:
        master_core = AuroraControl(master_core_uio, args.base_addr)
        slave_core = None if args.slave_uio_dev is None else AuroraControl(
            slave_core_uio,
            args.slave_base_addr,
        )
        if args.loopback:
            master_core.reset_core()
            master_core.set_loopback(enable=True)
            return True
        # Run BIST
        if args.test == 'ber':
            print("Performing BER BIST test.")
            master_core.run_ber_loopback_bist(
                args.duration,
                args.rate * 8e6,
                slave_core,
            )
        else:
            print("Performing Latency BIST test.")
            master_core.run_latency_loopback_bist(
                args.duration,
                args.rate * 8e6,
                slave_core,
            )
    except Exception as ex:
        print("Unexpected exception: {}".format(str(ex)))
        return False
    finally:
        master_core_uio.close()
        if args.slave_uio_dev:
            slave_core_uio.close()
Example #3
0
class LiberioDispatcherTable(object):
    """
    Controls a Liberio DMA dispatcher table.

    label -- A label that can be used by udev to find a UIO device
    """

    def __init__(self, label):
        self.log = get_logger(label)
        self._regs = UIO(label=label, read_only=False)
        self.poke32 = self._regs.poke32
        self.peek32 = self._regs.peek32

    def set_route(self, sid, dma_channel):
        """
        Sets up routing in the Liberio dispatcher. From sid, only the
        destination part is important. After this call, any CHDR packet with the
        appropriate destination address will get routed to `dma_channel`.

        sid -- Full SID, but only destination part matters.
        dma_channel -- The DMA channel to which these packets should get routed.
        """
        self.log.debug(
            "Routing SID `{sid}' to DMA channel `{chan}'.".format(
                sid=str(sid), chan=dma_channel
            )
        )
        def poke_and_trace(addr, data):
            " Do a poke32() and log.trace() "
            self.log.trace("Writing to address 0x{:04X}: 0x{:04X}".format(
                addr, data
            ))
            self.poke32(addr, data)
        # Poke reg for destination channel
        try:
            with self._regs.open():
                poke_and_trace(
                    0 + 4 * sid.dst_ep,
                    dma_channel,
                )
        except Exception as ex:
            self.log.error(
                "Unexpected exception while setting route: %s",
                str(ex),
            )
            raise
Example #4
0
class WhiteRabbitRegsControl(object):
    """
    Control and read the FPGA White Rabbit core registers
    """
    # Memory Map
    #  0x00000000: I/D Memory
    #  0x00020000: Peripheral interconnect
    #      +0x000: Minic
    #      +0x100: Endpoint
    #      +0x200: Softpll
    #      +0x300: PPS gen
    #      +0x400: Syscon
    #      +0x500: UART
    #      +0x600: OneWire
    #      +0x700: Auxillary space (Etherbone config, etc)
    #      +0x800: WRPC diagnostics registers
    PERIPH_INTERCON_BASE = 0x20000

    # PPS_GEN Map
    PPSG_ESCR = 0x31C

    def __init__(self, label, log):
        self.log = log
        self.regs = UIO(
            label=label,
            read_only=False
        )
        self.periph_peek32 = lambda addr: self.regs.peek32(addr + self.PERIPH_INTERCON_BASE)
        self.periph_poke32 = lambda addr, data: self.regs.poke32(addr + self.PERIPH_INTERCON_BASE, data)

    def get_time_lock_status(self):
        """
        Retrieves and decodes the lock status for the PPS out of the WR core.
        """
        with self.regs.open():
            ext_sync_status = self.periph_peek32(self.PPSG_ESCR)
        # bit 2: PPS_VALID
        # bit 3: TM_VALID (timecode)
        # All other bits MUST be ignored since they are not guaranteed to be zero or
        # stable!
        return (ext_sync_status & 0b1100) == 0b1100
Example #5
0
class MboardRegsControl(object):
    """
    Control the FPGA Motherboard registers
    """
    # Motherboard registers
    M_COMPAT_NUM = 0x0000
    MB_DATESTAMP = 0x0004
    MB_GIT_HASH = 0x0008
    MB_SCRATCH = 0x000C
    MB_NUM_CE = 0x0010
    MB_NUM_IO_CE = 0x0014
    MB_CLOCK_CTRL = 0x0018
    MB_XADC_RB = 0x001C
    MB_BUS_CLK_RATE = 0x0020
    MB_BUS_COUNTER = 0x0024
    MB_SFP0_INFO = 0x0028
    MB_SFP1_INFO = 0x002C
    MB_GPIO_MASTER = 0x0030
    MB_GPIO_RADIO_SRC = 0x0034

    # Bitfield locations for the MB_CLOCK_CTRL register.
    MB_CLOCK_CTRL_PPS_SEL_INT_10 = 0  # pps_sel is one-hot encoded!
    MB_CLOCK_CTRL_PPS_SEL_INT_25 = 1
    MB_CLOCK_CTRL_PPS_SEL_EXT = 2
    MB_CLOCK_CTRL_PPS_SEL_GPSDO = 3
    MB_CLOCK_CTRL_PPS_OUT_EN = 4  # output enabled = 1
    MB_CLOCK_CTRL_MEAS_CLK_RESET = 12  # set to 1 to reset mmcm, default is 0
    MB_CLOCK_CTRL_MEAS_CLK_LOCKED = 13  # locked indication for meas_clk mmcm

    def __init__(self, label, log):
        self.log = log
        self.regs = UIO(label=label, read_only=False)
        self.poke32 = self.regs.poke32
        self.peek32 = self.regs.peek32

    def get_compat_number(self):
        """get FPGA compat number

        This function reads back FPGA compat number.
        The return is a tuple of
        2 numbers: (major compat number, minor compat number )
        """
        with self.regs.open():
            compat_number = self.peek32(self.M_COMPAT_NUM)
        minor = compat_number & 0xff
        major = (compat_number >> 16) & 0xff
        return (major, minor)

    def set_fp_gpio_master(self, value):
        """set driver for front panel GPIO
        Arguments:
            value {unsigned} -- value is a single bit bit mask of 12 pins GPIO
        """
        with self.regs.open():
            return self.poke32(self.MB_GPIO_MASTER, value)

    def get_fp_gpio_master(self):
        """get "who" is driving front panel gpio
           The return value is a bit mask of 12 pins GPIO.
           0: means the pin is driven by PL
           1: means the pin is driven by PS
        """
        with self.regs.open():
            return self.peek32(self.MB_GPIO_MASTER) & 0xfff

    def set_fp_gpio_radio_src(self, value):
        """set driver for front panel GPIO
        Arguments:
            value {unsigned} -- value is 2-bit bit mask of 12 pins GPIO
           00: means the pin is driven by radio 0
           01: means the pin is driven by radio 1
           10: means the pin is driven by radio 2
           11: means the pin is driven by radio 3
        """
        with self.regs.open():
            return self.poke32(self.MB_GPIO_RADIO_SRC, value)

    def get_fp_gpio_radio_src(self):
        """get which radio is driving front panel gpio
           The return value is 2-bit bit mask of 12 pins GPIO.
           00: means the pin is driven by radio 0
           01: means the pin is driven by radio 1
           10: means the pin is driven by radio 2
           11: means the pin is driven by radio 3
        """
        with self.regs.open():
            return self.peek32(self.MB_GPIO_RADIO_SRC) & 0xffffff

    def get_build_timestamp(self):
        """
        Returns the build date/time for the FPGA image.
        The return is datetime string with the  ISO 8601 format
        (YYYY-MM-DD HH:MM:SS.mmmmmm)
        """
        with self.regs.open():
            datestamp_rb = self.peek32(self.MB_DATESTAMP)
        if datestamp_rb > 0:
            dt_str = datetime.datetime(year=((datestamp_rb >> 17) & 0x3F) +
                                       2000,
                                       month=(datestamp_rb >> 23) & 0x0F,
                                       day=(datestamp_rb >> 27) & 0x1F,
                                       hour=(datestamp_rb >> 12) & 0x1F,
                                       minute=(datestamp_rb >> 6) & 0x3F,
                                       second=((datestamp_rb >> 0) & 0x3F))
            self.log.trace("FPGA build timestamp: {}".format(str(dt_str)))
            return str(dt_str)
        else:
            # Compatibility with FPGAs without datestamp capability
            return ''

    def get_git_hash(self):
        """
        Returns the GIT hash for the FPGA build.
        The return is a tuple of
        2 numbers: (short git hash, bool: is the tree dirty?)
        """
        with self.regs.open():
            git_hash_rb = self.peek32(self.MB_GIT_HASH)
        git_hash = git_hash_rb & 0x0FFFFFFF
        tree_dirty = ((git_hash_rb & 0xF0000000) > 0)
        dirtiness_qualifier = 'dirty' if tree_dirty else 'clean'
        self.log.trace("FPGA build GIT Hash: {:07x} ({})".format(
            git_hash, dirtiness_qualifier))
        return (git_hash, dirtiness_qualifier)

    def set_time_source(self, time_source, ref_clk_freq):
        """
        Set time source
        """
        pps_sel_val = 0x0
        if time_source == 'internal':
            assert ref_clk_freq in (10e6, 25e6)
            if ref_clk_freq == 10e6:
                self.log.trace("Setting time source to internal "
                               "(10 MHz reference)...")
                pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_INT_10
            elif ref_clk_freq == 25e6:
                self.log.trace("Setting time source to internal "
                               "(25 MHz reference)...")
                pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_INT_25
        elif time_source == 'external':
            self.log.trace("Setting time source to external...")
            pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_EXT
        elif time_source == 'gpsdo':
            self.log.trace("Setting time source to gpsdo...")
            pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_GPSDO
        else:
            assert False
        with self.regs.open():
            reg_val = self.peek32(self.MB_CLOCK_CTRL) & 0xFFFFFFF0
            reg_val = reg_val | (pps_sel_val & 0xF)
            self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val))
            self.poke32(self.MB_CLOCK_CTRL, reg_val)

    def enable_pps_out(self, enable):
        """
        Enables the PPS/Trig output on the back panel
        """
        self.log.trace("%s PPS/Trig output!",
                       "Enabling" if enable else "Disabling")
        mask = 0xFFFFFFFF ^ (0b1 << self.MB_CLOCK_CTRL_PPS_OUT_EN)
        with self.regs.open():
            # mask the bit to clear it:
            reg_val = self.peek32(self.MB_CLOCK_CTRL) & mask
            if enable:
                # set the bit if desired:
                reg_val = reg_val | (0b1 << self.MB_CLOCK_CTRL_PPS_OUT_EN)
            self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val))
            self.poke32(self.MB_CLOCK_CTRL, reg_val)

    def reset_meas_clk_mmcm(self, reset=True):
        """
        Reset or unreset the MMCM for the measurement clock in the FPGA TDC.
        """
        self.log.trace("%s measurement clock MMCM reset...",
                       "Asserting" if reset else "Clearing")
        mask = 0xFFFFFFFF ^ (0b1 << self.MB_CLOCK_CTRL_MEAS_CLK_RESET)
        with self.regs.open():
            # mask the bit to clear it
            reg_val = self.peek32(self.MB_CLOCK_CTRL) & mask
            if reset:
                # set the bit if desired
                reg_val = reg_val | (0b1 << self.MB_CLOCK_CTRL_MEAS_CLK_RESET)
            self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val))
            self.poke32(self.MB_CLOCK_CTRL, reg_val)

    def get_meas_clock_mmcm_lock(self):
        """
        Check the status of the MMCM for the measurement clock in the FPGA TDC.
        """
        mask = 0b1 << self.MB_CLOCK_CTRL_MEAS_CLK_LOCKED
        with self.regs.open():
            reg_val = self.peek32(self.MB_CLOCK_CTRL)
        locked = (reg_val & mask) > 0
        if not locked:
            self.log.warning("Measurement clock MMCM reporting unlocked. "
                             "MB_CLOCK_CTRL reg: 0x{:08X}".format(reg_val))
        else:
            self.log.trace("Measurement clock MMCM locked!")
        return locked

    def get_fpga_type(self):
        """
        Reads the type of the FPGA image currently loaded
        Returns a string with the type (ie HG, XG, AA, etc.)
        """
        with self.regs.open():
            sfp0_info_rb = self.peek32(self.MB_SFP0_INFO)
            sfp1_info_rb = self.peek32(self.MB_SFP1_INFO)
        # Print the registers values as 32-bit hex values
        self.log.trace("SFP0 Info: 0x{0:0{1}X}".format(sfp0_info_rb, 8))
        self.log.trace("SFP1 Info: 0x{0:0{1}X}".format(sfp1_info_rb, 8))

        sfp0_type = N3XX_SFP_TYPES.get((sfp0_info_rb & 0x0000FF00) >> 8, "")
        sfp1_type = N3XX_SFP_TYPES.get((sfp1_info_rb & 0x0000FF00) >> 8, "")
        self.log.trace("SFP types: ({}, {})".format(sfp0_type, sfp1_type))
        if (sfp0_type == "") or (sfp1_type == ""):
            return ""
        elif (sfp0_type == "1G") and (sfp1_type == "10G"):
            return "HG"
        elif (sfp0_type == "10G") and (sfp1_type == "10G"):
            return "XG"
        elif (sfp0_type == "10G") and (sfp1_type == "A"):
            return "XA"
        elif (sfp0_type == "A") and (sfp1_type == "A"):
            return "AA"
        else:
            self.log.warning(
                "Unrecognized SFP type combination: ({}, {})".format(
                    sfp0_type, sfp1_type))
            return ""
Example #6
0
class EthDispatcherTable(object):
    """
    Controls an Ethernet dispatcher table.
    """
    DEFAULT_VITA_PORT = (49153, 49154)
    # Address offsets:
    OWN_IP_OFFSET = 0x0000
    OWN_PORT_OFFSET = 0x0004
    FORWARD_ETH_BCAST_OFFSET = 0x0008
    SID_IP_OFFSET = 0x1000
    SID_PORT_MAC_HI_OFFSET = 0x1400
    SID_MAC_LO_OFFSET = 0x1800

    def __init__(self, label):
        self.log = get_logger(label)
        self._regs = UIO(label=label, read_only=False)
        self.poke32 = self._regs.poke32
        self.peek32 = self._regs.peek32

    def set_ipv4_addr(self, ip_addr):
        """
        Set the own IPv4 address for this Ethernet dispatcher.
        Outgoing packets will have this IP address.
        """
        self.log.debug("Setting my own IP address to `{}'".format(ip_addr))
        ip_addr_int = int(netaddr.IPAddress(ip_addr))
        with self._regs.open():
            self.poke32(self.OWN_IP_OFFSET, ip_addr_int)

    def set_vita_port(self, port_value=None, port_idx=None):
        """
        Set the port that is used for incoming VITA traffic. This is used to
        distinguish traffic that goes to the FPGA from that going to the ARM.
        """
        port_idx = port_idx or 0
        port_value = port_value or self.DEFAULT_VITA_PORT[port_idx]
        assert port_idx in (0)  #FIXME: Fix port_idx = 1
        port_reg_addr = self.OWN_PORT_OFFSET
        with self._regs.open():
            self.poke32(port_reg_addr, port_value)

    def set_route(self, sid, ip_addr, udp_port, mac_addr=None):
        """
        Sets up routing in the Ethernet dispatcher. From sid, only the
        destination part is important. After this call, any CHDR packet
        reaching this Ethernet dispatcher will get routed to `ip_addr' and
        `udp_port'.

        It automatically looks up the MAC address of the destination unless a
        MAC address is given.

        sid -- Full SID, but only destination part matters.
        ip_addr -- IPv4 destination address. String format ("1.2.3.4").
        udp_addr -- Destination UDP port.
        mac_addr -- If given, this will replace an ARP lookup for the MAC
                    address. String format, ("aa:bb:cc:dd:ee:ff"), case does
                    not matter.
        """
        udp_port = int(udp_port)
        if mac_addr is None:
            mac_addr = get_mac_addr(ip_addr)
        if mac_addr is None:
            self.log.error(
                "Could not resolve a MAC address for IP address `{}'".format(
                    ip_addr))
        dst_ep = sid.dst_ep
        self.log.debug(
            "Routing SID `{sid}' (endpoint `{ep}') to IP address `{ip}', " \
            "MAC address `{mac}', port `{port}'".format(
                sid=str(sid),
                ep=dst_ep,
                ip=ip_addr,
                mac=mac_addr,
                port=udp_port
            )
        )
        ip_addr_int = int(netaddr.IPAddress(ip_addr))
        mac_addr_int = int(netaddr.EUI(mac_addr))
        sid_offset = 4 * dst_ep

        def poke_and_trace(addr, data):
            " Do a poke32() and log.trace() "
            self.log.trace("Writing to address 0x{:04X}: 0x{:04X}".format(
                addr, data))
            self.poke32(addr, data)

        with self._regs.open():
            poke_and_trace(self.SID_IP_OFFSET + sid_offset, ip_addr_int)
            poke_and_trace(
                self.SID_MAC_LO_OFFSET + sid_offset,
                mac_addr_int & 0xFFFFFFFF,
            )
            poke_and_trace(self.SID_PORT_MAC_HI_OFFSET + sid_offset,
                           (udp_port << 16) | (mac_addr_int >> 32))

    def set_forward_policy(self, forward_eth, forward_bcast):
        """
        Forward Ethernet packet not matching OWN_IP to CROSSOVER
        Forward broadcast packet to CPU and CROSSOVER
        """
        reg_value = int(bool(forward_eth) << 1) | int(bool(forward_bcast))
        self.log.trace("Writing to address 0x{:04X}: 0x{:04X}".format(
            self.FORWARD_ETH_BCAST_OFFSET, reg_value))
        with self._regs.open():
            self.poke32(self.FORWARD_ETH_BCAST_OFFSET, reg_value)
Example #7
0
class MboardRegsControl(object):
    """
    Control the FPGA Motherboard registers
    """
    # Motherboard registers
    M_COMPAT_NUM = 0x0000
    MB_DATESTAMP = 0x0004
    MB_GIT_HASH = 0x0008
    MB_SCRATCH = 0x000C
    MB_NUM_CE = 0x0010
    MB_NUM_IO_CE = 0x0014
    MB_CLOCK_CTRL = 0x0018
    MB_XADC_RB = 0x001C
    MB_BUS_CLK_RATE = 0x0020
    MB_BUS_COUNTER = 0x0024

    # Bitfield locations for the MB_CLOCK_CTRL register.
    MB_CLOCK_CTRL_PPS_SEL_INT_10 = 0  # pps_sel is one-hot encoded!
    MB_CLOCK_CTRL_PPS_SEL_INT_25 = 1
    MB_CLOCK_CTRL_PPS_SEL_EXT = 2
    MB_CLOCK_CTRL_PPS_SEL_GPSDO = 3
    MB_CLOCK_CTRL_PPS_OUT_EN = 4  # output enabled = 1
    MB_CLOCK_CTRL_MEAS_CLK_RESET = 12  # set to 1 to reset mmcm, default is 0
    MB_CLOCK_CTRL_MEAS_CLK_LOCKED = 13  # locked indication for meas_clk mmcm

    def __init__(self, label, log):
        self.log = log
        self.regs = UIO(label=label, read_only=False)
        self.poke32 = self.regs.poke32
        self.peek32 = self.regs.peek32

    def get_compat_number(self):
        """get FPGA compat number

        This function reads back FPGA compat number.
        The return is a tuple of
        2 numbers: (major compat number, minor compat number )
        """
        with self.regs.open():
            compat_number = self.peek32(self.M_COMPAT_NUM)
        minor = compat_number & 0xff
        major = (compat_number >> 16) & 0xff
        return (major, minor)

    def get_build_timestamp(self):
        """
        Returns the build date/time for the FPGA image.
        The return is datetime string with the  ISO 8601 format
        (YYYY-MM-DD HH:MM:SS.mmmmmm)
        """
        with self.regs.open():
            datestamp_rb = self.peek32(self.MB_DATESTAMP)
        if datestamp_rb > 0:
            dt_str = datetime.datetime(year=((datestamp_rb >> 17) & 0x3F) +
                                       2000,
                                       month=(datestamp_rb >> 23) & 0x0F,
                                       day=(datestamp_rb >> 27) & 0x1F,
                                       hour=(datestamp_rb >> 12) & 0x1F,
                                       minute=(datestamp_rb >> 6) & 0x3F,
                                       second=((datestamp_rb >> 0) & 0x3F))
            self.log.trace("FPGA build timestamp: {}".format(str(dt_str)))
            return str(dt_str)
        else:
            # Compatibility with FPGAs without datestamp capability
            return ''

    def get_git_hash(self):
        """
        Returns the GIT hash for the FPGA build.
        The return is a tuple of
        2 numbers: (short git hash, bool: is the tree dirty?)
        """
        with self.regs.open():
            git_hash_rb = self.peek32(self.MB_GIT_HASH)
        git_hash = git_hash_rb & 0x0FFFFFFF
        tree_dirty = ((git_hash_rb & 0xF0000000) > 0)
        dirtiness_qualifier = 'dirty' if tree_dirty else 'clean'
        self.log.trace("FPGA build GIT Hash: {:07x} ({})".format(
            git_hash, dirtiness_qualifier))
        return (git_hash, dirtiness_qualifier)

    def set_time_source(self, time_source, ref_clk_freq):
        """
        Set time source
        """
        pps_sel_val = 0x0
        if time_source == 'internal':
            assert ref_clk_freq in (10e6, 25e6)
            if ref_clk_freq == 10e6:
                self.log.trace("Setting time source to internal "
                               "(10 MHz reference)...")
                pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_INT_10
            elif ref_clk_freq == 25e6:
                self.log.trace("Setting time source to internal "
                               "(25 MHz reference)...")
                pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_INT_25
        elif time_source == 'external':
            self.log.trace("Setting time source to external...")
            pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_EXT
        elif time_source == 'gpsdo':
            self.log.trace("Setting time source to gpsdo...")
            pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_GPSDO
        else:
            assert False
        with self.regs.open():
            reg_val = self.peek32(self.MB_CLOCK_CTRL) & 0xFFFFFFF0
            reg_val = reg_val | (pps_sel_val & 0xF)
            self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val))
            self.poke32(self.MB_CLOCK_CTRL, reg_val)

    def enable_pps_out(self, enable):
        """
        Enables the PPS/Trig output on the back panel
        """
        self.log.trace("%s PPS/Trig output!",
                       "Enabling" if enable else "Disabling")
        mask = 0xFFFFFFFF ^ (0b1 << self.MB_CLOCK_CTRL_PPS_OUT_EN)
        with self.regs.open():
            # mask the bit to clear it:
            reg_val = self.peek32(self.MB_CLOCK_CTRL) & mask
            if enable:
                # set the bit if desired:
                reg_val = reg_val | (0b1 << self.MB_CLOCK_CTRL_PPS_OUT_EN)
            self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val))
            self.poke32(self.MB_CLOCK_CTRL, reg_val)

    def reset_meas_clk_mmcm(self, reset=True):
        """
        Reset or unreset the MMCM for the measurement clock in the FPGA TDC.
        """
        self.log.trace("%s measurement clock MMCM reset...",
                       "Asserting" if reset else "Clearing")
        mask = 0xFFFFFFFF ^ (0b1 << self.MB_CLOCK_CTRL_MEAS_CLK_RESET)
        with self.regs.open():
            # mask the bit to clear it
            reg_val = self.peek32(self.MB_CLOCK_CTRL) & mask
            if reset:
                # set the bit if desired
                reg_val = reg_val | (0b1 << self.MB_CLOCK_CTRL_MEAS_CLK_RESET)
            self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val))
            self.poke32(self.MB_CLOCK_CTRL, reg_val)

    def get_meas_clock_mmcm_lock(self):
        """
        Check the status of the MMCM for the measurement clock in the FPGA TDC.
        """
        mask = 0b1 << self.MB_CLOCK_CTRL_MEAS_CLK_LOCKED
        with self.regs.open():
            reg_val = self.peek32(self.MB_CLOCK_CTRL)
        locked = (reg_val & mask) > 0
        if not locked:
            self.log.warning("Measurement clock MMCM reporting unlocked. "
                             "MB_CLOCK_CTRL reg: 0x{:08X}".format(reg_val))
        else:
            self.log.trace("Measurement clock MMCM locked!")
        return locked