2018-02-21 23:00:28 +08:00
|
|
|
from numpy import int32, int64
|
|
|
|
|
2021-04-21 11:19:54 +08:00
|
|
|
from artiq.language.types import TInt32, TInt64, TFloat, TTuple, TBool
|
2018-02-21 23:00:28 +08:00
|
|
|
from artiq.language.core import kernel, delay, portable
|
2019-01-23 17:59:08 +08:00
|
|
|
from artiq.language.units import ms, us, ns
|
2018-01-02 21:52:13 +08:00
|
|
|
from artiq.coredevice.ad9912_reg import *
|
2017-11-25 01:47:46 +08:00
|
|
|
|
2018-02-21 23:00:28 +08:00
|
|
|
from artiq.coredevice import spi2 as spi
|
|
|
|
from artiq.coredevice import urukul
|
2017-11-25 01:47:46 +08:00
|
|
|
|
|
|
|
|
|
|
|
class AD9912:
|
|
|
|
"""
|
2018-02-14 16:45:17 +08:00
|
|
|
AD9912 DDS channel on Urukul
|
2017-11-25 01:47:46 +08:00
|
|
|
|
2018-02-14 16:45:17 +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
|
|
|
|
encoded chip select and not "one-hot".
|
2018-01-02 21:52:13 +08:00
|
|
|
:param cpld_device: Name of the Urukul CPLD this device is on.
|
2018-02-14 16:45:17 +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
|
2019-01-18 20:06:33 +08:00
|
|
|
f_ref/clk_div*pll_n where f_ref is the reference frequency and clk_div
|
|
|
|
is the reference clock divider (both set in the parent Urukul CPLD
|
|
|
|
instance).
|
2017-11-25 01:47:46 +08:00
|
|
|
"""
|
|
|
|
|
2018-01-02 21:52:13 +08:00
|
|
|
def __init__(self, dmgr, chip_select, cpld_device, sw_device=None,
|
2019-01-18 20:06:33 +08:00
|
|
|
pll_n=10):
|
2021-04-21 10:30:21 +08:00
|
|
|
self.kernel_invariants = {"cpld", "core", "bus", "chip_select",
|
|
|
|
"pll_n", "ftw_per_hz"}
|
2018-01-02 21:52:13 +08:00
|
|
|
self.cpld = dmgr.get(cpld_device)
|
|
|
|
self.core = self.cpld.core
|
|
|
|
self.bus = self.cpld.bus
|
2018-01-04 03:22:36 +08:00
|
|
|
assert 4 <= chip_select <= 7
|
2017-11-25 01:47:46 +08:00
|
|
|
self.chip_select = chip_select
|
2018-01-02 21:52:13 +08:00
|
|
|
if sw_device:
|
|
|
|
self.sw = dmgr.get(sw_device)
|
2018-03-08 03:00:50 +08:00
|
|
|
self.kernel_invariants.add("sw")
|
2018-01-02 21:52:13 +08:00
|
|
|
self.pll_n = pll_n
|
2021-03-15 11:39:12 +08:00
|
|
|
sysclk = self.cpld.refclk / [1, 1, 2, 4][self.cpld.clk_div] * pll_n
|
2019-01-18 20:06:33 +08:00
|
|
|
assert sysclk <= 1e9
|
2021-03-15 11:39:12 +08:00
|
|
|
self.ftw_per_hz = 1 / sysclk * (int64(1) << 48)
|
2017-11-25 01:47:46 +08:00
|
|
|
|
|
|
|
@kernel
|
2021-03-15 11:39:12 +08:00
|
|
|
def write(self, addr: TInt32, data: TInt32, length: TInt32):
|
2018-02-21 23:00:28 +08:00
|
|
|
"""Variable length write to a register.
|
|
|
|
Up to 4 bytes.
|
2018-02-14 16:45:17 +08:00
|
|
|
|
|
|
|
:param addr: Register address
|
|
|
|
:param data: Data to be written: int32
|
|
|
|
:param length: Length in bytes (1-4)
|
|
|
|
"""
|
2018-01-02 21:52:13 +08:00
|
|
|
assert length > 0
|
|
|
|
assert length <= 4
|
2018-02-21 23:00:28 +08:00
|
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG, 16,
|
2021-03-15 11:39:12 +08:00
|
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
2018-01-02 21:52:13 +08:00
|
|
|
self.bus.write((addr | ((length - 1) << 13)) << 16)
|
2021-03-15 11:39:12 +08:00
|
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, length * 8,
|
|
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
|
|
|
self.bus.write(data << (32 - length * 8))
|
2017-11-25 01:47:46 +08:00
|
|
|
|
2018-01-02 21:52:13 +08:00
|
|
|
@kernel
|
2021-03-15 11:39:12 +08:00
|
|
|
def read(self, addr: TInt32, length: TInt32) -> TInt32:
|
2018-02-21 23:00:28 +08:00
|
|
|
"""Variable length read from a register.
|
|
|
|
Up to 4 bytes.
|
2018-02-14 16:45:17 +08:00
|
|
|
|
|
|
|
:param addr: Register address
|
|
|
|
:param length: Length in bytes (1-4)
|
2018-02-21 23:00:28 +08:00
|
|
|
:return: Data read
|
2018-02-14 16:45:17 +08:00
|
|
|
"""
|
2018-01-02 21:52:13 +08:00
|
|
|
assert length > 0
|
|
|
|
assert length <= 4
|
2018-02-21 23:00:28 +08:00
|
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG, 16,
|
2021-03-15 11:39:12 +08:00
|
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
2018-01-02 21:52:13 +08:00
|
|
|
self.bus.write((addr | ((length - 1) << 13) | 0x8000) << 16)
|
2018-02-21 23:00:28 +08:00
|
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END
|
2021-03-15 11:39:12 +08:00
|
|
|
| spi.SPI_INPUT, length * 8,
|
|
|
|
urukul.SPIT_DDS_RD, self.chip_select)
|
2018-01-02 21:52:13 +08:00
|
|
|
self.bus.write(0)
|
2018-02-21 23:00:28 +08:00
|
|
|
data = self.bus.read()
|
2018-01-02 21:52:13 +08:00
|
|
|
if length < 4:
|
2021-03-15 11:39:12 +08:00
|
|
|
data &= (1 << (length * 8)) - 1
|
2018-01-02 21:52:13 +08:00
|
|
|
return data
|
2017-11-25 01:47:46 +08:00
|
|
|
|
2018-01-02 21:52:13 +08:00
|
|
|
@kernel
|
|
|
|
def init(self):
|
2018-02-21 23:00:28 +08:00
|
|
|
"""Initialize and configure the DDS.
|
|
|
|
|
|
|
|
Sets up SPI mode, confirms chip presence, powers down unused blocks,
|
|
|
|
and configures the PLL. Does not wait for PLL lock. Uses the
|
|
|
|
IO_UPDATE signal multiple times.
|
|
|
|
"""
|
2018-02-14 16:45:17 +08:00
|
|
|
# SPI mode
|
2018-02-21 23:00:28 +08:00
|
|
|
self.write(AD9912_SER_CONF, 0x99, length=1)
|
2021-03-15 11:39:12 +08:00
|
|
|
self.cpld.io_update.pulse(2 * us)
|
2018-02-14 16:45:17 +08:00
|
|
|
# Verify chip ID and presence
|
2018-01-02 21:52:13 +08:00
|
|
|
prodid = self.read(AD9912_PRODIDH, length=2)
|
2018-01-04 03:22:36 +08:00
|
|
|
if (prodid != 0x1982) and (prodid != 0x1902):
|
|
|
|
raise ValueError("Urukul AD9912 product id mismatch")
|
2021-03-15 11:39:12 +08:00
|
|
|
delay(50 * us)
|
2018-02-17 02:47:28 +08:00
|
|
|
# HSTL power down, CMOS power down
|
2018-02-21 23:00:28 +08:00
|
|
|
self.write(AD9912_PWRCNTRL1, 0x80, length=1)
|
2021-03-15 11:39:12 +08:00
|
|
|
self.cpld.io_update.pulse(2 * us)
|
|
|
|
self.write(AD9912_N_DIV, self.pll_n // 2 - 2, length=1)
|
|
|
|
self.cpld.io_update.pulse(2 * us)
|
2018-02-17 02:47:28 +08:00
|
|
|
# I_cp = 375 µA, VCO high range
|
2018-02-21 23:00:28 +08:00
|
|
|
self.write(AD9912_PLLCFG, 0b00000101, length=1)
|
2021-03-15 11:39:12 +08:00
|
|
|
self.cpld.io_update.pulse(2 * us)
|
|
|
|
delay(1 * ms)
|
2018-01-02 21:52:13 +08:00
|
|
|
|
|
|
|
@kernel
|
2021-03-15 11:39:12 +08:00
|
|
|
def set_att_mu(self, att: TInt32):
|
2018-02-14 16:45:17 +08:00
|
|
|
"""Set digital step attenuator in machine units.
|
|
|
|
|
2019-11-17 23:51:26 +08:00
|
|
|
This method will write the attenuator settings of all four channels.
|
|
|
|
|
2018-02-14 16:45:17 +08:00
|
|
|
.. seealso:: :meth:`artiq.coredevice.urukul.CPLD.set_att_mu`
|
|
|
|
|
|
|
|
:param att: Attenuation setting, 8 bit digital.
|
|
|
|
"""
|
2018-01-02 21:52:13 +08:00
|
|
|
self.cpld.set_att_mu(self.chip_select - 4, att)
|
2017-11-25 01:47:46 +08:00
|
|
|
|
|
|
|
@kernel
|
2021-03-15 11:39:12 +08:00
|
|
|
def set_att(self, att: TFloat):
|
2018-02-14 16:45:17 +08:00
|
|
|
"""Set digital step attenuator in SI units.
|
|
|
|
|
2019-11-17 23:51:26 +08:00
|
|
|
This method will write the attenuator settings of all four channels.
|
|
|
|
|
2018-02-14 16:45:17 +08:00
|
|
|
.. seealso:: :meth:`artiq.coredevice.urukul.CPLD.set_att`
|
|
|
|
|
2018-02-21 23:00:28 +08:00
|
|
|
:param att: Attenuation in dB. Higher values mean more attenuation.
|
2018-02-14 16:45:17 +08:00
|
|
|
"""
|
2018-01-02 21:52:13 +08:00
|
|
|
self.cpld.set_att(self.chip_select - 4, att)
|
2017-11-25 01:47:46 +08:00
|
|
|
|
2021-03-15 11:40:26 +08:00
|
|
|
@kernel
|
|
|
|
def get_att_mu(self) -> TInt32:
|
|
|
|
"""Get digital step attenuator value in machine units.
|
|
|
|
|
|
|
|
.. seealso:: :meth:`artiq.coredevice.urukul.CPLD.get_channel_att_mu`
|
|
|
|
|
|
|
|
:return: Attenuation setting, 8 bit digital.
|
|
|
|
"""
|
|
|
|
return self.cpld.get_channel_att_mu(self.chip_select - 4)
|
|
|
|
|
|
|
|
@kernel
|
|
|
|
def get_att(self) -> TFloat:
|
|
|
|
"""Get digital step attenuator value in SI units.
|
|
|
|
|
|
|
|
.. seealso:: :meth:`artiq.coredevice.urukul.CPLD.get_channel_att`
|
|
|
|
|
|
|
|
:return: Attenuation in dB.
|
|
|
|
"""
|
|
|
|
return self.cpld.get_channel_att(self.chip_select - 4)
|
|
|
|
|
2018-01-02 21:52:13 +08:00
|
|
|
@kernel
|
2021-03-15 11:39:12 +08:00
|
|
|
def set_mu(self, ftw: TInt64, pow_: TInt32):
|
2018-02-14 16:45:17 +08:00
|
|
|
"""Set profile 0 data in machine units.
|
|
|
|
|
|
|
|
After the SPI transfer, the shared IO update pin is pulsed to
|
|
|
|
activate the data.
|
|
|
|
|
2020-02-27 02:11:12 +08:00
|
|
|
:param ftw: Frequency tuning word: 48 bit unsigned.
|
2021-03-15 11:39:12 +08:00
|
|
|
:param pow_: Phase tuning word: 16 bit unsigned.
|
2018-02-14 16:45:17 +08:00
|
|
|
"""
|
|
|
|
# streaming transfer of FTW and POW
|
2018-02-21 23:00:28 +08:00
|
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG, 16,
|
2021-03-15 11:39:12 +08:00
|
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
2018-01-02 21:52:13 +08:00
|
|
|
self.bus.write((AD9912_POW1 << 16) | (3 << 29))
|
2018-02-21 23:00:28 +08:00
|
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG, 32,
|
2021-03-15 11:39:12 +08:00
|
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
|
|
|
self.bus.write((pow_ << 16) | (int32(ftw >> 32) & 0xffff))
|
2018-02-21 23:00:28 +08:00
|
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 32,
|
2021-03-15 11:39:12 +08:00
|
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
2018-01-02 21:52:13 +08:00
|
|
|
self.bus.write(int32(ftw))
|
2021-03-15 11:39:12 +08:00
|
|
|
self.cpld.io_update.pulse(10 * ns)
|
2018-01-02 21:52:13 +08:00
|
|
|
|
2021-03-15 11:40:26 +08:00
|
|
|
@kernel
|
|
|
|
def get_mu(self) -> TTuple([TInt64, TInt32]):
|
|
|
|
"""Get the frequency tuning word and phase offset word.
|
|
|
|
|
|
|
|
.. seealso:: :meth:`get`
|
|
|
|
|
|
|
|
:return: A tuple ``(ftw, pow)``.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Read data
|
|
|
|
high = self.read(AD9912_POW1, 4)
|
|
|
|
self.core.break_realtime() # Regain slack to perform second read
|
|
|
|
low = self.read(AD9912_FTW3, 4)
|
|
|
|
# Extract and return fields
|
|
|
|
ftw = (int64(high & 0xffff) << 32) | (int64(low) & int64(0xffffffff))
|
|
|
|
pow_ = (high >> 16) & 0x3fff
|
|
|
|
return ftw, pow_
|
|
|
|
|
2018-01-02 21:52:13 +08:00
|
|
|
@portable(flags={"fast-math"})
|
2021-03-15 11:39:12 +08:00
|
|
|
def frequency_to_ftw(self, frequency: TFloat) -> TInt64:
|
2020-05-17 21:09:11 +08:00
|
|
|
"""Returns the 48-bit frequency tuning word corresponding to the given
|
2018-01-02 21:52:13 +08:00
|
|
|
frequency.
|
|
|
|
"""
|
2021-03-15 11:39:12 +08:00
|
|
|
return int64(round(self.ftw_per_hz * frequency)) & (
|
|
|
|
(int64(1) << 48) - 1)
|
2018-01-02 21:52:13 +08:00
|
|
|
|
2018-08-02 19:19:04 +08:00
|
|
|
@portable(flags={"fast-math"})
|
2021-03-15 11:39:12 +08:00
|
|
|
def ftw_to_frequency(self, ftw: TInt64) -> TFloat:
|
2018-08-02 19:19:04 +08:00
|
|
|
"""Returns the frequency corresponding to the given
|
|
|
|
frequency tuning word.
|
|
|
|
"""
|
2021-03-15 11:39:12 +08:00
|
|
|
return ftw / self.ftw_per_hz
|
2018-08-02 19:19:04 +08:00
|
|
|
|
2018-01-02 21:52:13 +08:00
|
|
|
@portable(flags={"fast-math"})
|
2021-03-15 11:39:12 +08:00
|
|
|
def turns_to_pow(self, phase: TFloat) -> TInt32:
|
2020-05-17 21:09:11 +08:00
|
|
|
"""Returns the 16-bit phase offset word corresponding to the given
|
2018-01-02 21:52:13 +08:00
|
|
|
phase.
|
2017-11-25 01:47:46 +08:00
|
|
|
"""
|
2021-03-15 11:39:12 +08:00
|
|
|
return int32(round((1 << 14) * phase)) & 0xffff
|
|
|
|
|
|
|
|
@portable(flags={"fast-math"})
|
|
|
|
def pow_to_turns(self, pow_: TInt32) -> TFloat:
|
|
|
|
"""Return the phase in turns corresponding to a given phase offset
|
|
|
|
word.
|
|
|
|
|
|
|
|
:param pow_: Phase offset word.
|
|
|
|
:return: Phase in turns.
|
|
|
|
"""
|
|
|
|
return pow_ / (1 << 14)
|
2018-01-02 21:52:13 +08:00
|
|
|
|
|
|
|
@kernel
|
2021-03-15 11:39:12 +08:00
|
|
|
def set(self, frequency: TFloat, phase: TFloat = 0.0):
|
2018-02-14 16:45:17 +08:00
|
|
|
"""Set profile 0 data in SI units.
|
|
|
|
|
|
|
|
.. seealso:: :meth:`set_mu`
|
|
|
|
|
2021-03-15 11:39:12 +08:00
|
|
|
:param frequency: Frequency in Hz
|
|
|
|
:param phase: Phase tuning word in turns
|
2018-02-14 16:45:17 +08:00
|
|
|
"""
|
2018-01-02 21:52:13 +08:00
|
|
|
self.set_mu(self.frequency_to_ftw(frequency),
|
2021-03-15 11:39:12 +08:00
|
|
|
self.turns_to_pow(phase))
|
2018-10-24 19:01:13 +08:00
|
|
|
|
2021-03-15 11:40:26 +08:00
|
|
|
@kernel
|
|
|
|
def get(self) -> TTuple([TFloat, TFloat]):
|
|
|
|
"""Get the frequency and phase.
|
|
|
|
|
|
|
|
.. seealso:: :meth:`get_mu`
|
|
|
|
|
|
|
|
:return: A tuple ``(frequency, phase)``.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Get values
|
|
|
|
ftw, pow_ = self.get_mu()
|
|
|
|
# Convert and return
|
|
|
|
return self.ftw_to_frequency(ftw), self.pow_to_turns(pow_)
|
|
|
|
|
2018-10-24 19:01:13 +08:00
|
|
|
@kernel
|
2021-04-21 11:19:54 +08:00
|
|
|
def cfg_sw(self, state: TBool):
|
2018-10-24 19:01:13 +08:00
|
|
|
"""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)
|