2018-02-21 23:00:28 +08:00
|
|
|
from numpy import int32, int64
|
|
|
|
|
|
|
|
from artiq.language.core import kernel, delay, portable
|
2018-01-04 02:42:22 +08:00
|
|
|
from artiq.language.units import us, ns, ms
|
|
|
|
|
2018-02-21 23:00:28 +08:00
|
|
|
from artiq.coredevice import spi2 as spi
|
|
|
|
from artiq.coredevice import urukul
|
|
|
|
urukul_sta_pll_lock = urukul.urukul_sta_pll_lock
|
2018-10-26 17:00:07 +08:00
|
|
|
urukul_sta_smp_err = urukul.urukul_sta_smp_err
|
2018-01-04 02:42:22 +08:00
|
|
|
|
|
|
|
|
|
|
|
_AD9910_REG_CFR1 = 0x00
|
|
|
|
_AD9910_REG_CFR2 = 0x01
|
|
|
|
_AD9910_REG_CFR3 = 0x02
|
|
|
|
_AD9910_REG_AUX_DAC = 0x03
|
|
|
|
_AD9910_REG_IO_UPD = 0x04
|
|
|
|
_AD9910_REG_FTW = 0x07
|
|
|
|
_AD9910_REG_POW = 0x08
|
|
|
|
_AD9910_REG_ASF = 0x09
|
|
|
|
_AD9910_REG_MSYNC = 0x0A
|
|
|
|
_AD9910_REG_DRAMPL = 0x0B
|
|
|
|
_AD9910_REG_DRAMPS = 0x0C
|
|
|
|
_AD9910_REG_DRAMPR = 0x0D
|
|
|
|
_AD9910_REG_PR0 = 0x0E
|
|
|
|
_AD9910_REG_PR1 = 0x0F
|
|
|
|
_AD9910_REG_PR2 = 0x10
|
|
|
|
_AD9910_REG_PR3 = 0x11
|
|
|
|
_AD9910_REG_PR4 = 0x12
|
|
|
|
_AD9910_REG_PR5 = 0x13
|
|
|
|
_AD9910_REG_PR6 = 0x14
|
|
|
|
_AD9910_REG_PR7 = 0x15
|
|
|
|
_AD9910_REG_RAM = 0x16
|
|
|
|
|
|
|
|
|
|
|
|
class AD9910:
|
|
|
|
"""
|
2018-02-14 16:05:03 +08:00
|
|
|
AD9910 DDS channel on Urukul.
|
2018-01-04 02:42:22 +08:00
|
|
|
|
2018-02-14 16:05:03 +08:00
|
|
|
This class supports a single DDS channel and exposes the DDS,
|
|
|
|
the digital step attenuator, and the RF switch.
|
|
|
|
|
|
|
|
:param chip_select: Chip select configuration. On Urukul this is an
|
2018-07-29 08:55:51 +08:00
|
|
|
encoded chip select and not "one-hot": 3 to address multiple chips
|
|
|
|
(as configured through CFG_MASK_NU), 4-7 for individual channels.
|
2018-01-04 02:42:22 +08:00
|
|
|
:param cpld_device: Name of the Urukul CPLD this device is on.
|
2018-02-14 16:05:03 +08:00
|
|
|
:param sw_device: Name of the RF switch device. The RF switch is a
|
|
|
|
TTLOut channel available as the :attr:`sw` attribute of this instance.
|
|
|
|
:param pll_n: DDS PLL multiplier. The DDS sample clock is
|
|
|
|
f_ref/4*pll_n where f_ref is the reference frequency (set in the parent
|
|
|
|
Urukul CPLD instance).
|
|
|
|
:param pll_cp: DDS PLL charge pump setting.
|
|
|
|
:param pll_vco: DDS PLL VCO range selection.
|
2018-10-26 17:00:07 +08:00
|
|
|
:param sync_delay_seed: SYNC_IN delay tuning starting value.
|
|
|
|
To stabilize the SYNC_IN delay tuning, run :meth:`tune_sync_delay` once and
|
|
|
|
set this to the delay tap number returned by :meth:`tune_sync_delay`.
|
2018-01-04 02:42:22 +08:00
|
|
|
"""
|
2018-03-08 03:00:50 +08:00
|
|
|
kernel_invariants = {"chip_select", "cpld", "core", "bus",
|
2018-10-26 17:00:07 +08:00
|
|
|
"ftw_per_hz", "pll_n", "pll_cp", "pll_vco", "sync_delay_seed"}
|
2018-01-04 02:42:22 +08:00
|
|
|
|
|
|
|
def __init__(self, dmgr, chip_select, cpld_device, sw_device=None,
|
2018-10-26 17:00:07 +08:00
|
|
|
pll_n=40, pll_cp=7, pll_vco=5, sync_delay_seed=8):
|
2018-01-04 02:42:22 +08:00
|
|
|
self.cpld = dmgr.get(cpld_device)
|
|
|
|
self.core = self.cpld.core
|
|
|
|
self.bus = self.cpld.bus
|
2018-04-26 15:59:08 +08:00
|
|
|
assert 3 <= chip_select <= 7
|
2018-01-04 02:42:22 +08:00
|
|
|
self.chip_select = chip_select
|
|
|
|
if sw_device:
|
|
|
|
self.sw = dmgr.get(sw_device)
|
2018-03-08 03:00:50 +08:00
|
|
|
self.kernel_invariants.add("sw")
|
2018-01-04 02:42:22 +08:00
|
|
|
assert 12 <= pll_n <= 127
|
|
|
|
self.pll_n = pll_n
|
2018-02-21 23:00:28 +08:00
|
|
|
assert self.cpld.refclk/4 <= 60e6
|
|
|
|
sysclk = self.cpld.refclk*pll_n/4 # Urukul clock fanout divider
|
|
|
|
assert sysclk <= 1e9
|
|
|
|
self.ftw_per_hz = 1./sysclk*(int64(1) << 32)
|
2018-01-04 02:42:22 +08:00
|
|
|
assert 0 <= pll_vco <= 5
|
|
|
|
vco_min, vco_max = [(370, 510), (420, 590), (500, 700),
|
|
|
|
(600, 880), (700, 950), (820, 1150)][pll_vco]
|
2018-02-21 23:00:28 +08:00
|
|
|
assert vco_min <= sysclk/1e6 <= vco_max
|
2018-01-04 02:42:22 +08:00
|
|
|
self.pll_vco = pll_vco
|
|
|
|
assert 0 <= pll_cp <= 7
|
|
|
|
self.pll_cp = pll_cp
|
2018-10-26 17:00:07 +08:00
|
|
|
self.sync_delay_seed = sync_delay_seed
|
2018-01-04 02:42:22 +08:00
|
|
|
|
|
|
|
@kernel
|
2018-01-04 03:22:36 +08:00
|
|
|
def write32(self, addr, data):
|
2018-02-14 16:05:03 +08:00
|
|
|
"""Write to 32 bit register.
|
|
|
|
|
|
|
|
:param addr: Register address
|
|
|
|
:param data: Data to be written
|
|
|
|
"""
|
2018-02-21 23:00:28 +08:00
|
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG, 8,
|
|
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
2018-01-04 02:42:22 +08:00
|
|
|
self.bus.write(addr << 24)
|
2018-02-21 23:00:28 +08:00
|
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 32,
|
|
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
2018-01-04 03:22:36 +08:00
|
|
|
self.bus.write(data)
|
2018-02-21 23:00:28 +08:00
|
|
|
|
|
|
|
@kernel
|
|
|
|
def read32(self, addr):
|
|
|
|
"""Read from 32 bit register.
|
|
|
|
|
|
|
|
:param addr: Register address
|
|
|
|
"""
|
|
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG, 8,
|
|
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
|
|
|
self.bus.write((addr | 0x80) << 24)
|
|
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END
|
|
|
|
| spi.SPI_INPUT, 32,
|
|
|
|
urukul.SPIT_DDS_RD, self.chip_select)
|
|
|
|
self.bus.write(0)
|
|
|
|
return self.bus.read()
|
2018-01-04 02:42:22 +08:00
|
|
|
|
|
|
|
@kernel
|
|
|
|
def write64(self, addr, data_high, data_low):
|
2018-02-14 16:05:03 +08:00
|
|
|
"""Write to 64 bit register.
|
|
|
|
|
|
|
|
:param addr: Register address
|
|
|
|
:param data_high: High (MSB) 32 bits of the data
|
|
|
|
:param data_low: Low (LSB) 32 data bits
|
|
|
|
"""
|
2018-02-21 23:00:28 +08:00
|
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG, 8,
|
|
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
2018-01-04 02:42:22 +08:00
|
|
|
self.bus.write(addr << 24)
|
2018-02-21 23:00:28 +08:00
|
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG, 32,
|
|
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
2018-01-04 02:42:22 +08:00
|
|
|
self.bus.write(data_high)
|
2018-02-21 23:00:28 +08:00
|
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 32,
|
|
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
2018-01-04 02:42:22 +08:00
|
|
|
self.bus.write(data_low)
|
|
|
|
|
|
|
|
@kernel
|
2018-04-26 15:59:08 +08:00
|
|
|
def init(self, blind=False):
|
2018-02-21 23:00:28 +08:00
|
|
|
"""Initialize and configure the DDS.
|
2018-02-14 16:05:03 +08:00
|
|
|
|
2018-02-21 23:00:28 +08:00
|
|
|
Sets up SPI mode, confirms chip presence, powers down unused blocks,
|
|
|
|
configures the PLL, waits for PLL lock. Uses the
|
|
|
|
IO_UPDATE signal multiple times.
|
2018-04-26 15:59:08 +08:00
|
|
|
|
|
|
|
:param blind: Do not read back DDS identity and do not wait for lock.
|
2018-02-14 16:05:03 +08:00
|
|
|
"""
|
|
|
|
# Set SPI mode
|
2018-01-04 03:22:36 +08:00
|
|
|
self.write32(_AD9910_REG_CFR1, 0x00000002)
|
2018-10-26 00:02:13 +08:00
|
|
|
self.cpld.io_update.pulse(1*us)
|
2018-04-26 15:59:08 +08:00
|
|
|
delay(1*ms)
|
|
|
|
if not blind:
|
|
|
|
# Use the AUX DAC setting to identify and confirm presence
|
|
|
|
aux_dac = self.read32(_AD9910_REG_AUX_DAC)
|
|
|
|
if aux_dac & 0xff != 0x7f:
|
|
|
|
raise ValueError("Urukul AD9910 AUX_DAC mismatch")
|
|
|
|
delay(50*us) # slack
|
2018-02-14 16:05:03 +08:00
|
|
|
# Configure PLL settings and bring up PLL
|
2018-10-26 23:42:42 +08:00
|
|
|
self.write32(_AD9910_REG_CFR2, 0x01010020)
|
2018-10-26 00:02:13 +08:00
|
|
|
self.cpld.io_update.pulse(1*us)
|
2018-01-04 02:42:22 +08:00
|
|
|
cfr3 = (0x0807c100 | (self.pll_vco << 24) |
|
|
|
|
(self.pll_cp << 19) | (self.pll_n << 1))
|
2018-01-04 03:22:36 +08:00
|
|
|
self.write32(_AD9910_REG_CFR3, cfr3 | 0x400) # PFD reset
|
2018-10-26 00:02:13 +08:00
|
|
|
self.cpld.io_update.pulse(1*us)
|
2018-01-04 03:22:36 +08:00
|
|
|
self.write32(_AD9910_REG_CFR3, cfr3)
|
2018-10-26 00:02:13 +08:00
|
|
|
self.cpld.io_update.pulse(1*us)
|
2018-04-26 15:59:08 +08:00
|
|
|
if blind:
|
|
|
|
delay(100*ms)
|
|
|
|
return
|
2018-02-14 16:05:03 +08:00
|
|
|
# Wait for PLL lock, up to 100 ms
|
2018-01-04 02:42:22 +08:00
|
|
|
for i in range(100):
|
2018-02-21 23:00:28 +08:00
|
|
|
sta = self.cpld.sta_read()
|
|
|
|
lock = urukul_sta_pll_lock(sta)
|
2018-01-04 02:42:22 +08:00
|
|
|
delay(1*ms)
|
2018-02-21 23:00:28 +08:00
|
|
|
if lock & (1 << self.chip_select - 4):
|
2018-01-04 02:42:22 +08:00
|
|
|
return
|
2018-02-21 23:00:28 +08:00
|
|
|
raise ValueError("PLL lock timeout")
|
2018-01-04 02:42:22 +08:00
|
|
|
|
2018-10-26 00:02:54 +08:00
|
|
|
@kernel
|
|
|
|
def power_down(self, bits=0b1111):
|
|
|
|
"""Power down DDS.
|
|
|
|
|
|
|
|
:param bits: power down bits, see datasheet
|
|
|
|
"""
|
|
|
|
self.write32(_AD9910_REG_CFR1, 0x00000002 | (bits << 4))
|
|
|
|
self.cpld.io_update.pulse(1*us)
|
|
|
|
|
2018-01-04 02:42:22 +08:00
|
|
|
@kernel
|
2018-02-21 23:00:28 +08:00
|
|
|
def set_mu(self, ftw, pow=0, asf=0x3fff):
|
2018-02-14 16:05:03 +08:00
|
|
|
"""Set profile 0 data in machine units.
|
|
|
|
|
|
|
|
After the SPI transfer, the shared IO update pin is pulsed to
|
|
|
|
activate the data.
|
|
|
|
|
2018-02-21 23:00:28 +08:00
|
|
|
:param ftw: Frequency tuning word: 32 bit.
|
2018-02-14 16:05:03 +08:00
|
|
|
:param pow: Phase tuning word: 16 bit unsigned.
|
|
|
|
:param asf: Amplitude scale factor: 14 bit unsigned.
|
|
|
|
"""
|
2018-01-04 02:42:22 +08:00
|
|
|
self.write64(_AD9910_REG_PR0, (asf << 16) | pow, ftw)
|
2018-10-26 00:02:13 +08:00
|
|
|
self.cpld.io_update.pulse_mu(8)
|
2018-01-04 02:42:22 +08:00
|
|
|
|
|
|
|
@portable(flags={"fast-math"})
|
|
|
|
def frequency_to_ftw(self, frequency):
|
|
|
|
"""Returns the frequency tuning word corresponding to the given
|
|
|
|
frequency.
|
|
|
|
"""
|
|
|
|
return int32(round(self.ftw_per_hz*frequency))
|
|
|
|
|
|
|
|
@portable(flags={"fast-math"})
|
|
|
|
def turns_to_pow(self, turns):
|
|
|
|
"""Returns the phase offset word corresponding to the given phase
|
|
|
|
in turns."""
|
|
|
|
return int32(round(turns*0x10000))
|
|
|
|
|
|
|
|
@portable(flags={"fast-math"})
|
|
|
|
def amplitude_to_asf(self, amplitude):
|
|
|
|
"""Returns amplitude scale factor corresponding to given amplitude."""
|
|
|
|
return int32(round(amplitude*0x3ffe))
|
|
|
|
|
|
|
|
@kernel
|
|
|
|
def set(self, frequency, phase=0.0, amplitude=1.0):
|
2018-02-14 16:05:03 +08:00
|
|
|
"""Set profile 0 data in SI units.
|
|
|
|
|
|
|
|
.. seealso:: :meth:`set_mu`
|
|
|
|
|
|
|
|
:param ftw: Frequency in Hz
|
|
|
|
:param pow: Phase tuning word in turns
|
|
|
|
:param asf: Amplitude in units of full scale
|
|
|
|
"""
|
2018-01-04 02:42:22 +08:00
|
|
|
self.set_mu(self.frequency_to_ftw(frequency),
|
|
|
|
self.turns_to_pow(phase),
|
|
|
|
self.amplitude_to_asf(amplitude))
|
|
|
|
|
|
|
|
@kernel
|
2018-02-21 23:00:28 +08:00
|
|
|
def set_att_mu(self, att):
|
2018-02-14 16:05:03 +08:00
|
|
|
"""Set digital step attenuator in machine units.
|
|
|
|
|
2018-02-14 16:45:17 +08:00
|
|
|
.. seealso:: :meth:`artiq.coredevice.urukul.CPLD.set_att_mu`
|
|
|
|
|
2018-02-14 16:05:03 +08:00
|
|
|
:param att: Attenuation setting, 8 bit digital.
|
|
|
|
"""
|
2018-01-04 02:42:22 +08:00
|
|
|
self.cpld.set_att_mu(self.chip_select - 4, att)
|
|
|
|
|
|
|
|
@kernel
|
|
|
|
def set_att(self, att):
|
2018-02-14 16:05:03 +08:00
|
|
|
"""Set digital step attenuator in SI units.
|
|
|
|
|
2018-02-14 16:45:17 +08:00
|
|
|
.. seealso:: :meth:`artiq.coredevice.urukul.CPLD.set_att`
|
|
|
|
|
2018-02-14 16:05:03 +08:00
|
|
|
:param att: Attenuation in dB.
|
|
|
|
"""
|
2018-01-04 02:42:22 +08:00
|
|
|
self.cpld.set_att(self.chip_select - 4, att)
|
2018-10-24 19:01:13 +08:00
|
|
|
|
|
|
|
@kernel
|
|
|
|
def cfg_sw(self, state):
|
|
|
|
"""Set CPLD CFG RF switch state. The RF switch is controlled by the
|
|
|
|
logical or of the CPLD configuration shift register
|
|
|
|
RF switch bit and the SW TTL line (if used).
|
|
|
|
|
|
|
|
:param state: CPLD CFG RF switch bit
|
|
|
|
"""
|
|
|
|
self.cpld.cfg_sw(self.chip_select - 4, state)
|
2018-10-26 17:00:07 +08:00
|
|
|
|
|
|
|
@kernel
|
2018-10-26 23:42:42 +08:00
|
|
|
def set_sync(self, in_delay, window):
|
|
|
|
"""Set the relevant parameters in the multi device synchronization
|
|
|
|
register. See the AD9910 datasheet for details. The SYNC clock
|
|
|
|
generator preset value is set to zero, and the SYNC_OUT generator is
|
|
|
|
disabled.
|
|
|
|
|
|
|
|
:param in_delay: SYNC_IN delay tap (0-31) in steps of ~75ps
|
|
|
|
:param window: Symmetric SYNC_IN validation window (0-15) in
|
|
|
|
steps of ~75ps for both hold and setup margin.
|
|
|
|
"""
|
2018-10-26 17:00:07 +08:00
|
|
|
self.write32(_AD9910_REG_MSYNC,
|
|
|
|
(window << 28) | # SYNC S/H validation delay
|
|
|
|
(1 << 27) | # SYNC receiver enable
|
|
|
|
(0 << 26) | # SYNC generator disable
|
|
|
|
(0 << 25) | # SYNC generator SYS rising edge
|
2018-10-26 23:42:42 +08:00
|
|
|
(0 << 18) | # SYNC preset
|
2018-10-26 17:00:07 +08:00
|
|
|
(0 << 11) | # SYNC output delay
|
|
|
|
(in_delay << 3)) # SYNC receiver delay
|
2018-10-26 23:42:42 +08:00
|
|
|
|
|
|
|
@kernel
|
|
|
|
def clear_smp_err(self):
|
|
|
|
"""Clears the SMP_ERR flag and enables SMP_ERR validity monitoring.
|
|
|
|
Violations of the SYNC_IN sample and hold margins will result in
|
|
|
|
SMP_ERR being asserted. This then also activates the red LED on
|
|
|
|
the respective Urukul channel.
|
|
|
|
|
|
|
|
Also modifies CFR2.
|
|
|
|
"""
|
|
|
|
self.write32(_AD9910_REG_CFR2, 0x01010020) # clear SMP_ERR
|
2018-10-26 17:00:07 +08:00
|
|
|
self.cpld.io_update.pulse(1*us)
|
2018-10-26 23:42:42 +08:00
|
|
|
self.write32(_AD9910_REG_CFR2, 0x01010000) # enable SMP_ERR
|
2018-10-26 17:00:07 +08:00
|
|
|
self.cpld.io_update.pulse(1*us)
|
|
|
|
|
|
|
|
@kernel
|
|
|
|
def tune_sync_delay(self):
|
2018-10-26 23:42:42 +08:00
|
|
|
"""Find a stable SYNC_IN delay.
|
|
|
|
|
|
|
|
This method first locates the smallest SYNC_IN validity window at
|
|
|
|
minimum window size and then increases the window a bit to provide some
|
|
|
|
slack and stability.
|
|
|
|
|
|
|
|
It starts scanning delays around :attr:`sync_delay_seed` (see the
|
|
|
|
device database arguments and :meth:`__init__`) at maximum validation window
|
|
|
|
size and decreases the window size until a valid delay is found.
|
|
|
|
|
|
|
|
:return: Tuple of optimal delay and window size.
|
|
|
|
"""
|
2018-10-26 17:00:07 +08:00
|
|
|
dt = 14 # 1/(f_SYSCLK*75ps) taps per SYSCLK period
|
|
|
|
max_delay = dt # 14*75ps > 1ns
|
|
|
|
max_window = dt//4 + 1 # 2*75ps*4 = 600ps high > 1ns/2
|
|
|
|
min_window = dt//8 + 1 # 2*75ps hold, 2*75ps setup < 1ns/4
|
|
|
|
for window in range(max_window - min_window + 1):
|
|
|
|
window = max_window - window
|
|
|
|
for in_delay in range(max_delay):
|
|
|
|
# alternate search direction around seed_delay
|
|
|
|
if in_delay & 1:
|
|
|
|
in_delay = -in_delay
|
2018-10-26 23:42:42 +08:00
|
|
|
in_delay = self.sync_delay_seed + (in_delay >> 1)
|
|
|
|
if in_delay < 0:
|
|
|
|
in_delay = 0
|
|
|
|
elif in_delay > 31:
|
|
|
|
in_delay = 31
|
2018-10-26 17:00:07 +08:00
|
|
|
self.set_sync(in_delay, window)
|
2018-10-26 23:42:42 +08:00
|
|
|
self.clear_smp_err()
|
2018-10-26 17:00:07 +08:00
|
|
|
# integrate SMP_ERR statistics for a few hundred cycles
|
|
|
|
delay(10*us)
|
|
|
|
err = urukul_sta_smp_err(self.cpld.sta_read())
|
|
|
|
err = (err >> (self.chip_select - 4)) & 1
|
|
|
|
delay(40*us) # slack
|
|
|
|
if not err:
|
|
|
|
window -= min_window # add margin
|
|
|
|
self.set_sync(in_delay, window)
|
2018-10-26 23:42:42 +08:00
|
|
|
self.clear_smp_err()
|
|
|
|
delay(40*us) # slack
|
|
|
|
return in_delay, window
|
2018-10-26 17:00:07 +08:00
|
|
|
raise ValueError("no valid window/delay")
|