uhd/mpm/python/usrp_mpm/dboard_manager/magnesium.py
djepson1 4e0600b00a mg: Updated JESD204b init seq and documentation.
- Based on feedback from ADI, updated SYSREF sequencing for
    meeting deterministic latency requirements.
  - Changed majority of register addresses in nijesdcore.py to
    constants.
  - Corrected write data to SYSREF_CAPTURE_CONTROL to produce
    the correct SYSREF toggle rate inside the FPGA.

Signed-off-by: djepson1 <daniel.jepson@ni.com>
2017-12-22 15:04:02 -08:00

404 lines
15 KiB
Python

#
# Copyright 2017 Ettus Research (National Instruments)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
magnesium dboard implementation module
"""
from __future__ import print_function
import struct
import time
from six import iteritems
from . import lib # Pulls in everything from C++-land
from .base import DboardManagerBase
from .. import nijesdcore
from ..uio import UIO
from ..mpmlog import get_logger
from .lmk_mg import LMK04828Mg
from usrp_mpm.cores import ClockSynchronizer
def create_spidev_iface(dev_node):
"""
Create a regs iface from a spidev node
"""
SPI_SPEED_HZ = 1000000
SPI_MODE = 3
SPI_ADDR_SHIFT = 8
SPI_DATA_SHIFT = 0
SPI_READ_FLAG = 1<<23
SPI_WRIT_FLAG = 0
return lib.spi.make_spidev_regs_iface(
dev_node,
SPI_SPEED_HZ,
SPI_MODE,
SPI_ADDR_SHIFT,
SPI_DATA_SHIFT,
SPI_READ_FLAG,
SPI_WRIT_FLAG
)
def create_spidev_iface_cpld(dev_node):
"""
Create a regs iface from a spidev node
"""
SPI_SPEED_HZ = 1000000
SPI_MODE = 0
SPI_ADDR_SHIFT = 16
SPI_DATA_SHIFT = 0
SPI_READ_FLAG = 1<<23
SPI_WRIT_FLAG = 0
return lib.spi.make_spidev_regs_iface(
dev_node,
SPI_SPEED_HZ,
SPI_MODE,
SPI_ADDR_SHIFT,
SPI_DATA_SHIFT,
SPI_READ_FLAG,
SPI_WRIT_FLAG
)
def create_spidev_iface_phasedac(dev_node):
"""
Create a regs iface from a spidev node (ADS5681)
"""
return lib.spi.make_spidev_regs_iface(
str(dev_node),
1000000, # Speed (Hz)
1, # SPI mode
16, # Addr shift
0, # Data shift
0, # Read flag
0, # Write flag
)
class Magnesium(DboardManagerBase):
"""
Holds all dboard specific information and methods of the magnesium dboard
"""
#########################################################################
# Overridables
#
# See DboardManagerBase for documentation on these fields
#########################################################################
pids = [0x150]
# Maps the chipselects to the corresponding devices:
spi_chipselect = {"cpld": 0, "lmk": 1, "mykonos": 2, "phase_dac": 3}
spi_factories = {
"cpld": create_spidev_iface_cpld,
"lmk": create_spidev_iface,
"mykonos": create_spidev_iface,
"phase_dac": create_spidev_iface_phasedac,
}
# DAC is initialized to midscale automatically on power-on: 16-bit DAC, so midpoint
# is at 2^15 = 32768. However, the linearity of the DAC is best just below that
# point, so we set it to the (carefully calculated) alternate value instead.
INIT_PHASE_DAC_WORD = 31000 # Intentionally decimal
def _get_mykonos_function(self, name):
mykfunc = getattr(self.mykonos, name)
def func(*args):
return mykfunc(*args)
func.__doc__ = mykfunc.__doc__
return func
def __init__(self, slot_idx, **kwargs):
super(Magnesium, self).__init__(slot_idx, **kwargs)
self.log = get_logger("Magnesium-{}".format(slot_idx))
self.log.trace("Initializing Magnesium daughterboard, slot index {}".format(self.slot_idx))
self.ref_clock_freq = 10e6 # TODO: make this not fixed
self.log.debug("Loading C++ drivers...")
self._device = lib.dboards.magnesium_manager(
self._spi_nodes['mykonos'],
)
self.mykonos = self._device.get_radio_ctrl()
self.log.debug("Loaded C++ drivers.")
for mykfuncname in [x for x in dir(self.mykonos) if not x.startswith("_") and callable(getattr(self.mykonos, x))]:
self.log.trace("adding {}".format(mykfuncname))
setattr(self, mykfuncname, self._get_mykonos_function(mykfuncname))
def init(self, args):
"""
Execute necessary init dance to bring up dboard
"""
def _init_dboard_regs():
" Create a UIO object to talk to dboard regs "
self.log.trace("Getting uio...")
return UIO(
label="dboard-regs-{}".format(self.slot_idx),
read_only=False
)
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_clock_control(dboard_regs):
" Create a dboard clock control object and reset it "
dboard_clk_control = DboardClockControl(dboard_regs, self.log)
dboard_clk_control.reset_mmcm()
return dboard_clk_control
def _init_lmk(slot_idx, lmk_spi, ref_clk_freq,
pdac_spi, init_phase_dac_word):
"""
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(0x0, init_phase_dac_word)
self.spi_lock = self._device.get_spi_lock()
return LMK04828Mg(lmk_spi, self.spi_lock, ref_clk_freq, slot_idx)
def _sync_db_clock(synchronizer):
" Synchronizes the DB clock to the common reference "
synchronizer.run_sync(measurement_only=False)
offset_error = synchronizer.run_sync(measurement_only=True)
if offset_error > 100e-12:
self.log.error("Clock synchronizer measured an offset of {:.1f} ps!".format(
offset_error*1e12
))
raise RuntimeError("Clock synchronizer measured an offset of {:.1f} ps!".format(
offset_error*1e12
))
else:
self.log.debug("Residual DAC offset error: {:.1f} ps.".format(
offset_error*1e12
))
self.log.info("Sample Clock Synchronization Complete!")
def _init_cpld():
"Initialize communication with the Mg CPLD"
CPLD_SIGNATURE = 0xCAFE
cpld_regs = self._spi_ifaces['cpld']
signature = cpld_regs.peek16(0x00)
self.log.trace("CPLD Signature: 0x{:X}".format(signature))
if signature != CPLD_SIGNATURE:
self.log.error("CPLD Signature Mismatch! Expected: 0x{:x}".format(CPLD_SIGNATURE))
raise RuntimeError("CPLD Status Check Failed!")
self.log.trace("CPLD Revision: 0d{}".format(cpld_regs.peek16(0x01)))
revision_msb = cpld_regs.peek16(0x04)
self.log.trace("CPLD Date Code: 0x{:X}".format(cpld_regs.peek16(0x03) | (revision_msb << 16)))
return cpld_regs
self.log.info("init() called with args `{}'".format(
",".join(['{}={}'.format(x, args[x]) for x in args])
))
self.radio_regs = _init_dboard_regs()
self.log.info("Radio-register UIO object successfully generated!")
self._spi_ifaces = _init_spi_devices()
self.log.info("Loaded SPI interfaces!")
self.cpld_regs = _init_cpld()
self.dboard_clk_control = _init_clock_control(self.radio_regs)
self.lmk = _init_lmk(
self.slot_idx,
self._spi_ifaces['lmk'],
self.ref_clock_freq,
self._spi_ifaces['phase_dac'],
self.INIT_PHASE_DAC_WORD,
)
self.dboard_clk_control.enable_mmcm()
self.log.info("Sample Clocks and Phase DAC Configured Successfully!")
# Synchronize DB Clocks
self.clock_synchronizer = ClockSynchronizer(
self.radio_regs,
self.dboard_clk_control,
self.lmk,
self._spi_ifaces['phase_dac'],
0, # TODO this might not actually be zero
125e6, # TODO don't hardcode
self.ref_clock_freq,
860E-15, # TODO don't hardcode. This should live in the EEPROM
self.INIT_PHASE_DAC_WORD,
3e9, # lmk_vco_freq
[128e-9,], # target_values
0x0, # spi_addr TODO: make this a constant and replace in _sync_db_clock as well
self.log
)
_sync_db_clock(self.clock_synchronizer)
# Clocks and PPS are now fully active!
self.init_jesd(self.radio_regs)
self.mykonos.start_radio()
return True
def cpld_peek(self, addr):
"""
Debug for accessing the CPLD via the RPC shell.
"""
self.cpld_regs = create_spidev_iface_cpld(self._spi_nodes['cpld'])
self.log.trace("CPLD Signature: 0x{:X}".format(self.cpld_regs.peek16(0x00)))
revision_msb = self.cpld_regs.peek16(0x04)
self.log.trace("CPLD Revision: 0x{:X}".format(self.cpld_regs.peek16(0x03) | (revision_msb << 16)))
return self.cpld_regs.peek16(addr)
def cpld_poke(self, addr, data):
"""
Debug for accessing the CPLD via the RPC shell.
"""
self.cpld_regs = create_spidev_iface_cpld(self._spi_nodes['cpld'])
self.log.trace("CPLD Signature: 0x{:X}".format(self.cpld_regs.peek16(0x00)))
revision_msb = self.cpld_regs.peek16(0x04)
self.log.trace("CPLD Revision: 0x{:X}".format(self.cpld_regs.peek16(0x03) | (revision_msb << 16)))
self.cpld_regs.poke16(addr, data)
return self.cpld_regs.peek16(addr)
def init_jesd(self, uio):
"""
Bring up the JESD link between Mykonos and the N310.
"""
# CPLD Register Definition
MYKONOS_CONTROL = 0x13
self.log.trace("Creating jesdcore object")
self.jesdcore = nijesdcore.NIMgJESDCore(uio, self.slot_idx)
self.jesdcore.check_core()
self.jesdcore.unreset_qpll()
self.jesdcore.init()
self.log.trace("Pulsing Mykonos Hard Reset...")
self.cpld_regs.poke16(MYKONOS_CONTROL, 0x1)
time.sleep(0.001) # No spec here, but give it some time to reset.
self.cpld_regs.poke16(MYKONOS_CONTROL, 0x0)
time.sleep(0.001) # No spec here, but give it some time to enable.
self.log.trace("Initializing Mykonos...")
self.mykonos.begin_initialization()
# Multi-chip Sync requires two SYSREF pulses at least 17us apart.
self.jesdcore.send_sysref_pulse()
time.sleep(0.001)
self.jesdcore.send_sysref_pulse()
self.mykonos.finish_initialization()
self.log.trace("Starting JESD204b Link Initialization...")
# Generally, enable the source before the sink. Start with the DAC side.
self.log.trace("Starting FPGA framer...")
self.jesdcore.init_framer()
self.log.trace("Starting Mykonos deframer...")
self.mykonos.start_jesd_rx()
# Now for the ADC link. Note that the Mykonos framer will not start issuing CGS
# characters until SYSREF is received by the framer. Therefore we enable the
# framer in Mykonos and the FPGA, send a SYSREF pulse to everyone, and then
# start the deframer in the FPGA.
self.log.trace("Starting Mykonos framer...")
self.mykonos.start_jesd_tx()
self.log.trace("Enable FPGA SYSREF Receiver.")
self.jesdcore.enable_lmfc()
self.jesdcore.send_sysref_pulse()
self.log.trace("Starting FPGA deframer...")
self.jesdcore.init_deframer()
# Allow a bit of time for CGS/ILA to complete.
time.sleep(0.100)
if not self.jesdcore.get_framer_status():
self.log.error("FPGA Framer Error!")
raise Exception('JESD Core Framer is not synced!')
if ((self.mykonos.get_deframer_status() & 0x7F) != 0x28):
self.log.error("Mykonos Deframer Error: 0x{:X}".format((self.mykonos.get_deframer_status() & 0x7F)))
raise Exception('Mykonos Deframer is not synced!')
if not self.jesdcore.get_deframer_status():
self.log.error("FPGA Deframer Error!")
raise Exception('JESD Core Deframer is not synced!')
if ((self.mykonos.get_framer_status() & 0xFF) != 0x3E):
self.log.error("Mykonos Framer Error: 0x{:X}".format((self.mykonos.get_framer_status() & 0xFF)))
raise Exception('Mykonos Framer is not synced!')
if ((self.mykonos.get_multichip_sync_status() & 0xB) != 0xB):
raise Exception('Mykonos multi chip sync failed!')
self.log.info("JESD204B Link Initialization & Training Complete")
def dump_jesd_core(self):
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("")
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)
time.sleep(0.5) # Replace with poll and timeout TODO
mmcm_locked = bool(self.peek32(self.RADIO_CLK_MMCM) & 0x10)
if not mmcm_locked:
self.log.error("MMCM not locked!")
raise RuntimeError("MMCM not locked!")
self.log.trace("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)