from __future__ import annotations
from atomiq.components.primitives import Component, Switchable
from atomiq.components.electronics.rfsource import RFSource
from atomiq.helper import identity_float, identity_float_int32, replace_member
from artiq.experiment import kernel, delay, delay_mu, parallel
from artiq.language.types import TFloat, TBool, TInt32, TList
from artiq.language.units import us
from artiq.coredevice import ad9910
from artiq.coredevice import urukul
from artiq.coredevice import spi2 as spi
import artiq
[docs]
class Urukul(Component):
"""Sinara Urukul 4 Channel DDS
This class represents the Sinara Urukul 4 channel DDS RF source.
Args:
cpld: The ARTIQ cpld device from the `device_db`, e.g. `@urukul0_cpld`.
default_profile: Which profile in the Urukul CPLD to use by default, i.e. if no profile is given (default 7)
"""
kernel_invariants = {"cpld"}
def __init__(self, cpld: artiq.coredevice.urukul.CPLD, default_profile: TInt32 = 7, *args, **kwargs):
Component.__init__(self, *args, **kwargs)
self.cpld = cpld
self.profile = default_profile
@kernel
def _prerun(self):
self.core.break_realtime()
delay(250 * us)
delay(10 * us)
self.cpld.get_att_mu()
self.set_profile(self.profile, trigger=False)
[docs]
@kernel
def set_profile(self, profile: TInt32 = ad9910.DEFAULT_PROFILE, trigger: TBool = True):
"""
Set the Urukul to the given profile.
:param TInt32 profile: profile
:param TBool trigger: pulse io_update
"""
self.cpld.set_profile(profile)
if trigger:
self.cpld.io_update.pulse_mu(8)
[docs]
class UrukulChannel(RFSource, Switchable):
"""Single DDS Channel of a Sinara Urukul
Args:
urukul: The Urukul component this channel belongs to
device: The ARTIQ device from the `device_db` representing the Urukul channel, e.g. `@urukul0_ch0`
ttl: The ARTIQ device from the `device_db` representing the Urukul fast RF switch, e.g. `@ttl_urukul0_sw0`
default_attenuation: Default attenuation to set for the channel on startup. (default -19dBm)
profile_arb: Profile on the DDS to use for arbitrary function generation. (default 0)
"""
kernel_invariants = {"urukul", "device", "ttl", "profile_default", "profile_arb"}
def __init__(self, urukul: Urukul, device, ttl=None, default_attenuation=19.0, profile_arb: TInt32 = 0,
*args, **kwargs):
RFSource.__init__(self, *args, **kwargs)
Switchable.__init__(self, ["RF_on"])
self.urukul = urukul
self.device = device
if ttl is not None:
self.ttl = ttl
else:
replace_member(self, "on", "_on_cpld")
replace_member(self, "off", "_off_cpld")
self.ttl = None
self.attenuation = default_attenuation
self.profile_arb = profile_arb
self.profile_default = ad9910.DEFAULT_PROFILE
@kernel
def _prerun(self):
delay(250 * us) # ATH, 150 -> 250
self.off()
self.device.set_phase_mode(ad9910.PHASE_MODE_CONTINUOUS)
# attenuation
delay(150 * us)
self.set_att(self.attenuation)
# DDS
delay(150 * us)
self.set(
frequency=self.frequency, phase=0.0, amplitude=self.amplitude, profile=-1
)
[docs]
@kernel
def set(
self,
frequency: TFloat = -1.0,
amplitude: TFloat = -1.0,
phase: TFloat = 0.0,
profile: TInt32 = -1,
):
"""
Set the frequency and amplitude.
Frequency/amplitude are set to the last known value if -1 is given.
Args:
frequency: frequency [Hz]
amplitude: amplitude
"""
self.frequency = self.frequency if frequency < 0 else frequency
self.amplitude = self.amplitude if amplitude < 0 else amplitude
profile = self.profile_default if profile < 0 else profile
self.device.set(
frequency=self.frequency,
amplitude=self.amplitude,
phase=phase,
profile=profile,
)
# also write to the arb profile
self.device.set(
frequency=self.frequency,
amplitude=self.amplitude,
phase=phase,
profile=self.profile_arb,
)
[docs]
@kernel
def set_att(self, attenuation: TFloat):
"""
Set the hardware attenuation for this urukul channel via cpld.
Args:
attenuation: channel attenuation (0. to 31.0 in 0.5 increments) [dB]
"""
self.attenuation = attenuation
self.device.set_att(attenuation)
# self.kernel_log(self.attenuation)
delay_mu(30)
@kernel
def _set_frequency(self, frequency: TFloat):
self.set(frequency=frequency)
@kernel
def _set_amplitude(self, amplitude: TFloat):
self.set(amplitude=amplitude)
@kernel
def _set_phase(self, phase: TFloat):
self.set(phase=phase)
[docs]
@kernel
def on(self):
"""
Turn on via ttl.
:return
"""
delay_mu(4)
self.ttl.on()
delay_mu(4)
[docs]
@kernel
def off(self):
"""
Turn off via ttl.
:return
"""
delay_mu(4)
self.ttl.off()
delay_mu(4)
@kernel
def _on_cpld(self):
self.device.cpld.cfg_sw(self.device.chip_select-4, True)
delay_mu(30)
@kernel
def _off_cpld(self):
self.device.cpld.cfg_sw(self.device.chip_select-4, False)
delay_mu(30)
@kernel(flags={"fast-math"})
def _ramp(self,
duration: TFloat,
frequency_start: TFloat,
frequency_end: TFloat,
amplitude_start: TFloat,
amplitude_end: TFloat,
ramp_timestep: TFloat = 200e-6):
"""
At some point, we want to replace this by code that uses the digital ramp generator (DRG) on the AD9910 because
this allows for much faster ramps
"""
if ramp_timestep < 3.0*us:
ramp_timestep = 3.0 * us
self.experiment.log.warning(self.identifier + ": Ramp timestep {0:.3f} below limit (3.0us). Reducing...",
[ramp_timestep*1e6])
n = int(duration / ramp_timestep)
ramp_timestep = duration / float(n)
if n < 2:
self.experiment.log.error(self.identifier + ": Ramp impossible with duration {0}us and ramp_timestep {1}us",
[duration*1e6, ramp_timestep*1e6])
freq_step = (frequency_end - frequency_start) / n
amp_step = (amplitude_end - amplitude_start) / n
delay(-1.31*us)
for i in range(n):
_freq = frequency_start + i * freq_step
_amp = amplitude_start + i * amp_step
with parallel:
self.set(frequency=_freq, amplitude=_amp)
delay(ramp_timestep)
@kernel
def _write_ram(self, data: TList(TFloat), transform=identity_float_int32):
"""Write data to RAM.
.. note::
This is copied from upstream artiq to fix a bug, where the data is written into the
RAM in reversed order. Once this is fixed upstream, we can remove this
The profile to write to and the step, start, and end address
need to be configured before and separately using
:meth:`set_profile_ram` and the parent CPLD `set_profile`.
:param data: Data to be written to RAM.
"""
_AD9910_REG_RAM = 0x16
self.device.bus.set_config_mu(urukul.SPI_CONFIG, 8, urukul.SPIT_DDS_WR,
self.device.chip_select)
self.device.bus.write(_AD9910_REG_RAM << 24)
self.device.bus.set_config_mu(urukul.SPI_CONFIG, 32,
urukul.SPIT_DDS_WR, self.device.chip_select)
i = len(data) - 1
while i > 0:
self.device.bus.write(transform(data[i]))
i -= 1
self.device.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 32,
urukul.SPIT_DDS_WR, self.device.chip_select)
self.device.bus.write(transform(data[0]))
@kernel
def _arb(self,
duration: TFloat,
samples_amp: TList(TFloat),
samples_freq: TList(TFloat),
samples_phase: TList(TFloat),
repetitions: TInt32 = 1,
prepare_only: TBool = False,
run_prepared: TBool = False,
transform_amp=identity_float,
transform_freq=identity_float,
transform_phase=identity_float,
profile: TInt32 = 0
):
# initialize to make the compiler happy
ram_destination = 0
t_step = 0.0
_transform_amp = identity_float_int32
_transform_freq = identity_float_int32
_transform_phase = identity_float_int32
num_samples = max(len(samples_amp), max(len(samples_freq), len(samples_phase)))
t_step = duration/float(num_samples)
if len(samples_amp) + len(samples_freq) + len(samples_phase) != num_samples:
self.experiment.log.warning(self.identifier + ": Trying ARB on more than one parameter (amp, freq, phase) "
"but Urukul only supports one at a time. Falling back to software ARB.")
# RFSource._arb(duration, samples_amp, samples_freq, samples_phase, repetitions)
else:
if t_step < 4e-9:
self.experiment.log.error(self.identifier + ": ARB timestep {0:.1f} ns below minimum (4ns). Decrease "
"number of samples or increase duration", [t_step*1e9])
if t_step < 40e-9:
self.experiment.log.warning(self.identifier + ": ARB timestep {0:.1f} ns close to minimum (4ns). "
"Rounding errors will occur", [t_step*1e9])
if not run_prepared:
delay(-(5.0 + 0.535*num_samples)*us)
if len(samples_amp) > 0:
@kernel
def _transform_amp(x):
return self.device.amplitude_to_asf(transform_amp(x)) << 18
self._prepare_arb(samples_amp, t_step, repetitions > 1, profile=self.profile_arb,
transform=_transform_amp)
elif len(samples_freq) > 0:
@kernel
def _transform_freq(x):
return self.device.frequency_to_ftw(transform_freq(x))
self._prepare_arb(samples_freq, t_step, repetitions > 1, profile=self.profile_arb,
transform=_transform_freq)
elif len(samples_phase) > 0:
@kernel
def _transform_phase(x):
return self.device.turns_to_pow(transform_phase(x)) << 16
self._prepare_arb(samples_phase, t_step, repetitions > 1, profile=self.profile_arb,
transform=_transform_phase)
if prepare_only:
self.device.cpld.set_profile(self.profile_default)
else:
delay(2.3*us)
if not prepare_only:
delay(-2.3*us)
if len(samples_amp) > 0:
ram_destination = ad9910.RAM_DEST_ASF
elif len(samples_freq) > 0:
ram_destination = ad9910.RAM_DEST_FTW
elif len(samples_phase) > 0:
ram_destination = ad9910.RAM_DEST_POW
self._run_arb(repetitions*t_step*num_samples, profile=self.profile_arb, ram_destination=ram_destination)
@kernel
def _prepare_arb(self,
samples: TList(TFloat),
t_step: TFloat,
repeat: TBool = False,
ram_offset: TInt32 = 0,
profile: TInt32 = 0,
transform=identity_float_int32) -> TInt32:
"""
Prepare a RAM profile for arbitrary amplitude modulation with amplitude values and equidistant time steps.
Args:
samples: List of sample values in units im machine units. Maximum lenght is 1024 samples.
t_step: Time that should pass between the samples of the amplitudes list in units of s.
repeat: Should the sequence of samples be repeated? If not the last value is hold. (default False)
ram_offset: Address offset of the RAM storage address. (default 0)
profile: Profile of the DDS to use for the RAM mode. If none is given, profile 0 is used.
"""
num_samples = len(samples)
# disable RAM for writing data
self.device.set_cfr1(ram_enable=0)
self.device.cpld.io_update.pulse_mu(8)
self.device.cpld.set_profile(profile)
# write to RAM
self.device.set_profile_ram(
start=ram_offset,
end=ram_offset + num_samples - 1,
step=round(t_step / 4e-9),
profile=profile,
mode=ad9910.RAM_MODE_CONT_RAMPUP if repeat else ad9910.RAM_MODE_RAMPUP,
nodwell_high=0,
)
self.device.cpld.io_update.pulse_mu(8)
# write data to RAM, write only len(num_samples) words
# self.device.write_ram(samples[:num_samples])
self._write_ram(samples[:num_samples], transform=transform)
return num_samples - 1 + ram_offset
@kernel
def _run_arb(self, duration: TFloat, ram_destination: TInt32, trigger: TBool = True, profile: TInt32 = -1):
self.device.set(frequency=self.frequency, amplitude=self.amplitude, phase=self.phase,
ram_destination=ram_destination)
if ram_destination != ad9910.RAM_DEST_ASF:
# work around for https://github.com/m-labs/artiq/issues/1554: Activate OSK
self.device.set_cfr1(ram_enable=1, ram_destination=ram_destination, manual_osk_external=0,
osk_enable=1, select_auto_osk=0)
else:
self.device.set_cfr1(ram_enable=1, ram_destination=ram_destination)
# switch_profile:
self.device.cpld.set_profile(profile if profile >= 0 else self.profile_arb)
if trigger:
self.device.cpld.io_update.pulse_mu(8)
delay(duration)
# disable RAM mode
delay(-0.8*us)
self.device.set_cfr1(ram_enable=0)
# switch_profile:
self.device.cpld.set_profile(self.profile_default)
if trigger:
self.device.cpld.io_update.pulse_mu(8)