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, isnan, less_than_uint32, unsigned_bitshift32
from artiq.experiment import kernel, delay, delay_mu, parallel
from artiq.language.types import TFloat, TBool, TInt32, TList, TInt64, TStr
from artiq.language.units import us, ms
from artiq.language.core import now_mu, at_mu
from artiq.coredevice import ad9910
from artiq.coredevice import urukul
from artiq.coredevice import spi2 as spi
import artiq
import numpy as np
[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)
use_drg: Use the DRG for amplitude or frequency ramps (recommended). (default True)
auto_sync_drg: Defined if the drg is synced to either frequency (1) or amplitude (2) in the prepare phase.
Default is not synced (0). Syncing is recommended in combination with :attr:`blind` if continuous operation
is required.
Tip:
The :meth:`ramp` method can be used in combination with the :code:`blind` component argument. This allows for generating
continous, jump-free ramps between experiments. This makes it possible to use the dds output to generate a
lock signal for a laser cavity lock.
Attention:
Setting parameters before the DRG is synced with the python object would break the continuity.
Run :meth:`sync_drg` before setting amplitude, frequency or phase parameters or use :attr:`auto_sync_drg`.
"""
kernel_invariants = {"urukul", "device", "ttl", "profile_default", "profile_arb"}
def __init__(
self, urukul: Urukul, device: TStr, ttl=None,
default_attenuation=19.0, profile_arb: TInt32 = 0,
use_drg: TBool = True, auto_sync_drg: 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
# _drg_state = -1: DRG not synced but continous operation requested, takes last drg values as start point
# _drg_state = 0 : DRG not synced, no continuous operation requested
# _drg_state = 1 : DRG synced to self.frequency
# _drg_state = 2 : DRG synced to self.amplitude
self.use_drg = use_drg
self.auto_sync_drg = auto_sync_drg
if self.blind:
self._drg_state = -1
else:
self._drg_state = 0
[docs]
@kernel
def init(self):
"""
Initialize the urukul channel. Necessary only once after power on.
Warning:
This function is not called automatically as it is slow and resets the device leading to a
frequency/amplitude jump.
"""
sta = urukul.urukul_sta_rf_sw(self.device.cpld.sta_read())
if int(sta) == 0:
#cpld not initialized yet
delay(10 * us)
self.device.cpld.init()
self.device.init()
@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
print(self._drg_state, self.auto_sync_drg)
if self._drg_state == 0:
# do not set if continous drg mode without jumps is active
delay(150 * us)
self.set(
frequency=self.frequency, phase=0.0, amplitude=self.amplitude, profile=-1
)
elif self._drg_state == -1 and self.auto_sync_drg > 0:
self.sync_drg(self.auto_sync_drg)
@kernel
def _prerun_blind(self):
if self._drg_state == -1 and self.auto_sync_drg > 0:
self.sync_drg(self.auto_sync_drg)
[docs]
@kernel
def set(
self,
frequency: TFloat = float("nan"),
amplitude: TFloat = float("nan"),
phase: TFloat = 0.0,
profile: TInt32 = -1,
):
"""
Set the frequency and amplitude.
Frequency/amplitude are set to the last known value if ``float("nan")`` is passed (default).
Args:
frequency: Frequency in Hz (``float("nan")`` to use previous value)
amplitude: Amplitude in units of full scale (``float("nan")`` to use previous value)
phase: Phase tuning word in turns
profile: DDS Profile (``-1`` to keep current profile)
"""
if not isnan(frequency):
self.frequency = frequency
if self._drg_state == 1:
self._drg_state = 0
if not isnan(amplitude):
self.amplitude = amplitude
if self._drg_state == 2:
self._drg_state = 0
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)
[docs]
@kernel
def amplitude_to_drg_step(self, val):
return np.int32(round(0xffffffff*val))
[docs]
@kernel
def read64_32(self, addr: TInt32):
""" Read two 32-bit values from 64-bit register. Advances time cursor by 8600ns
Args:
addr: Register address
Returns:
Tuple of two 32-bit integer register values
"""
self.device.bus.set_config_mu(
urukul.SPI_CONFIG, 8,
urukul.SPIT_DDS_WR, self.device.chip_select)
self.device.bus.write((addr | 0x80) << 24)
self.device.bus.set_config_mu(
urukul.SPI_CONFIG | spi.SPI_INPUT, 32,
urukul.SPIT_DDS_RD, self.device.chip_select)
self.device.bus.write(0)
self.device.bus.set_config_mu(
urukul.SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, 32,
urukul.SPIT_DDS_RD, self.device.chip_select)
self.device.bus.write(0)
hi = self.device.bus.read()
lo = self.device.bus.read()
return hi, lo
@kernel
def _drg_write_registers(self,ramp_low, ramp_high, step_word):
"""
Advances time cursor by 2500ns
"""
self.device.write64(
ad9910._AD9910_REG_RAMP_LIMIT,
ramp_high,
ramp_low,
)
self.device.write64(
ad9910._AD9910_REG_RAMP_STEP,
step_word, # destination parameter (here FTW) decrement (DRCTL = 0)
0, # destination parameter increment (DRCTL = 1)
)
[docs]
@kernel
def sync_drg(self, target: TInt32):
"""
Synchronises the python object with the current register values of the DRG.
Advances time cursor by 25us.
Args:
target: Target to synchronise to. 1 for frequency, 2 for amplitude.
"""
old_high, old_low = self.read64_32(ad9910._AD9910_REG_RAMP_LIMIT)
delay_mu(7800)
last_ramp_step, _ = self.read64_32(ad9910._AD9910_REG_RAMP_STEP)
if last_ramp_step > 0:
# Ramp was going down before, DRG is in lower limit
current_mu = old_low
else:
# Ramp was going up before, DRG is in upper limit
current_mu = old_high
if target == 1:
self.frequency = self.device.ftw_to_frequency(current_mu)
self._drg_state = 1
elif target == 2:
self.amplitude = self.device.asf_to_amplitude(unsigned_bitshift32(current_mu,18))
self._drg_state = 2
@kernel
def _write_drg_ramp(self, ramp_rate, end_val, old_low, old_high, step):
start_time = now_mu()
last_ramp_step = np.int32(self.device.read64(ad9910._AD9910_REG_RAMP_STEP) >> 32)
hold_accumulator_down = False
delay(10*us)
# calculate and set ramp rate to work around the non-exposed ramp direction pin
self.device.write32(
ad9910._AD9910_REG_RAMP_RATE,
ramp_rate << 16, # set negative slope rate
)
delay(5*us)
if last_ramp_step > 0:
if less_than_uint32(end_val,old_low):
# Last ramp was down, current ramp is down
ramp_high = old_low
ramp_low = end_val
step_word = step+1
delay_mu(-8)
# We need to bring the DRG to its upper limit first to avoid a jump
self._drg_write_registers(old_low,old_low-1, -1*1<<31)
self.device.cpld.io_update.pulse_mu(8)
elif less_than_uint32(old_low,end_val):
# Last ramp was down, current ramp is up
ramp_high = end_val
ramp_low = old_low
step_word = -step
hold_accumulator_down = True
else:
# Start frequency same as end frequency, no ramp necessary
return
else:
if less_than_uint32(end_val,old_high):
# Last ramp was up, current ramp is down
step_word = step
ramp_high = old_high
ramp_low = end_val
delay_mu(-8)
# We need to make sure the DRG is in its upper limit first to avoid a jump
self._drg_write_registers(old_high,old_high-1, -1*1<<31)
self.device.cpld.io_update.pulse_mu(8)
elif less_than_uint32(old_high, end_val):
# Last ramp was up, current ramp is up
step_word = -step
ramp_high = end_val
ramp_low = old_high
hold_accumulator_down = True
else:
# Start frequency same as end frequency, no ramp necessary
return
delay(10*us)
self._drg_write_registers(ramp_low, ramp_high, step_word)
if hold_accumulator_down:
self.device.write32(ad9910._AD9910_REG_CFR1, (1 << 12) | 2)
self.device.cpld.io_update.pulse_mu(8)
at_mu(start_time + 40000)
@kernel
def _amplitude_ramp_drg(self,
duration: TFloat,
amplitude_start: TFloat,
amplitude_end: TFloat,
ramp_timestep: TFloat = 200e-6):
delay(-66.5*us)
sync_lim_register = False
if amplitude_start != self.amplitude and self._drg_state >= 0:
# Fixed ramp from start to finish, we don't care about jumps
start_asf = max(self.device.amplitude_to_asf(amplitude_start),1) << 18
old_low_asf = start_asf
old_high_asf = old_low_asf
sync_lim_register = True
delay_mu(2*8600) # compensate for read delay in other cases
elif self._drg_state < 0:
# Use the current DRG state as starting point
delay_mu(-7000)
old_high_asf, old_low_asf = self.read64_32(ad9910._AD9910_REG_RAMP_LIMIT)
delay_mu(7000)
last_ramp_step, _ = self.read64_32(ad9910._AD9910_REG_RAMP_STEP)
if last_ramp_step > 0:
# Ramp was going down before, DRG is in lower limit
start_asf = old_low_asf
else:
# Ramp was going up before, DRG is in upper limit
start_asf = old_high_asf
amplitude_start = self.device.asf_to_amplitude(unsigned_bitshift32(start_asf,18))
else:
# Start from the current amplitude set in the component
old_high_asf, old_low_asf = self.read64_32(ad9910._AD9910_REG_RAMP_LIMIT)
start_asf = max(self.device.amplitude_to_asf(self.amplitude),1) << 18
delay_mu(8600) # compensate for read delay in other cases
if (self._drg_state != 2 or sync_lim_register) and self._drg_state >= 0:
# DRG limit register is not synced with current amplitude
old_low_asf = start_asf
old_high_asf = start_asf
if ramp_timestep > 262e-6:
delay_mu(10000) # We need this delay to shedule the RPC log call
self.experiment.log.warning(self.identifier + " Amplitude ramp step length is too long for using the DRG. Must be" \
" <= 262us but is {0:.2f}, automatically reducing to 260us. This Warning introduces a delay!" \
" Set 'ramp_timestep' or the component property 'default_ramp_steps' to a smaller value!",
[ramp_timestep*1e6,])
ramp_timestep = 260e-6
# Setting the ramp limit to 0 leads to undefind behavior -> cut
end_asf = max(self.device.amplitude_to_asf(amplitude_end),1) << 18
amp_step = abs(amplitude_end - amplitude_start) /(duration / ramp_timestep -1)
ramp_rate = int(round(self.device.sysclk*ramp_timestep/4))
cfr2 = (
(1 << 21) | # DRG destination = amplitude, see AD9910 datasheet table 11
(1 << 20) | # DRG destination = amplitude, see AD9910 datasheet table 11
(1 << 19) | # enable DRG
(1 << 24) | # asf_profile enabled
(1 << 16) # enable effective ftw
)
delay_mu(7780)
self.device.write32(ad9910._AD9910_REG_CFR2, cfr2)
self._write_drg_ramp(ramp_rate, end_asf, old_low_asf, old_high_asf, self.device.amplitude_to_asf(amp_step) << 18)
self.device.set_cfr1(drg_load_lrr=1)
self.device.cpld.io_update.pulse_mu(8)
with parallel:
# minimum delay for very short ramps to avoid underflow
delay(duration)
delay_mu(10000)
self.set_amplitude(amplitude_end)
self.device.set_cfr2() # Switch ramp generator off again
self.device.set_cfr1()
self._drg_state = 2 # DRG registers now synced with amplitude
delay_mu(-4000)
@kernel
def _frequency_ramp_drg(self,
duration: TFloat,
frequency_start: TFloat,
frequency_end: TFloat,
ramp_timestep: TFloat):
delay(-66.5*us)
sync_lim_register = False
if frequency_start != self.frequency and self._drg_state >= 0:
#fixed ramp from start to finish, we don't care about jumps
old_low_ftw = self.device.frequency_to_ftw(frequency_start)
old_high_ftw = old_low_ftw
sync_lim_register = True
start_ftw = self.device.frequency_to_ftw(frequency_start)
delay_mu(2*8600) # compensate for read delay in other cases
elif self._drg_state < 0:
# use the current DRG state as starting point
delay_mu(-7000)
old_high_ftw, old_low_ftw = self.read64_32(ad9910._AD9910_REG_RAMP_LIMIT)
delay_mu(7000)
last_ramp_step, _ = self.read64_32(ad9910._AD9910_REG_RAMP_STEP)
if last_ramp_step > 0:
# ramp was going down before, drg is in lower limit
start_ftw = old_low_ftw
else:
# ramp was going up before, drg is in upper limit
start_ftw = old_high_ftw
frequency_start = self.device.ftw_to_frequency(start_ftw)
else:
#start from the current frequency
old_high_ftw, old_low_ftw = self.read64_32(ad9910._AD9910_REG_RAMP_LIMIT)
start_ftw = self.device.frequency_to_ftw(self.frequency)
delay_mu(8600) # compensate for read delay in other cases
if (self._drg_state != 1 or sync_lim_register) and self._drg_state >= 0:
# drg limit register is not synced with current frequency
old_low_ftw = start_ftw
old_high_ftw = start_ftw
if ramp_timestep > 262e-6:
delay_mu(10000)
self.experiment.log.warning(self.identifier + " Frequency ramp step length is too long for using the DRG. Must be" \
" <= 262us but is {0:.2f}, automatically reducing to 260us. This Warning introduces a delay!" \
" Set 'ramp_timestep' or the component property 'default_ramp_steps' to a smaller value!",
[ramp_timestep*1e6,])
ramp_timestep = 260e-6
end_ftw = self.device.frequency_to_ftw(frequency_end)
freq_step = abs(frequency_end - frequency_start) *ramp_timestep / duration
ramp_rate = int(round(self.device.sysclk*ramp_timestep/4))
cfr2 = (
(0 << 20) | # DRG destination = FTW, see AD9910 datasheet table 11
(0 << 21) | # DRG destination = FTW, see AD9910 datasheet table 11
(1 << 19) | # enable DRG
(1 << 24) | # asf_profile enabled
(1 << 16) # enable effective ftw
)
delay_mu(7780)
self.device.write32(ad9910._AD9910_REG_CFR2, cfr2)
self._write_drg_ramp(ramp_rate, end_ftw, old_low_ftw, old_high_ftw, self.device.frequency_to_ftw(freq_step))
self.device.set_cfr1(drg_load_lrr=1)
self.device.cpld.io_update.pulse_mu(8)
with parallel:
delay(duration)
delay_mu(10000)
self.set_frequency(frequency_end)
self.device.set_cfr2() # switch ramp generator off again
self.device.set_cfr1()
self._drg_state = 1 # drg registers now synced with frequency
delay_mu(-4000)
@kernel(flags={"fast-math"})
def _ramp(self,
duration: TFloat,
frequency_start: TFloat,
frequency_end: TFloat,
amplitude_start: TFloat,
amplitude_end: TFloat,
ramp_timestep: TFloat = 50e-6):
if amplitude_start == self.amplitude and amplitude_end == self.amplitude and self.use_drg:
if frequency_start != self.frequency or frequency_end != self.frequency or self._drg_state == -1:
# pure frequency ramp, use drg
self._frequency_ramp_drg(duration, frequency_start, frequency_end, ramp_timestep)
else:
# only do something if there is a change in frequency
with parallel:
delay(duration)
delay(10*us)
elif frequency_start == self.frequency and frequency_end == self.frequency and self.use_drg:
if amplitude_start != self.amplitude or amplitude_end != self.amplitude or self._drg_state == -1:
# pure amplitude ramp, use drg
self._amplitude_ramp_drg(duration, amplitude_start, amplitude_end, ramp_timestep)
else:
# only do something if there is a change in amplitude
with parallel:
delay(duration)
delay(10*us)
else:
# frequency and amplitude ramp, we can't use the drg as it dosn't support parallel ramps or drg disabled
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 - 1)
amp_step = (amplitude_end - amplitude_start) / (n - 1)
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)
[docs]
@kernel(flags={"fast-math"})
def ramp(
self,
duration: TFloat,
frequency_start: TFloat = float("nan"),
frequency_end: TFloat = float("nan"),
amplitude_start: TFloat = float("nan"),
amplitude_end: TFloat = float("nan"),
ramp_timestep: TFloat = float("nan"),
ramp_steps: TInt32 = -1
):
"""
Ramp frequency and amplitude over a given duration.
Parameters default to ``-1`` or ``nan`` to indicate no change.
If the start frequency/amplitude is set to ``nan``, the ramp starts from the last frequency/amplitude which was set.
This method advances the timeline by ´duration´
Tip:
This method can be used in combination with the :code:`blind` component argument. This allows for generating
continous, jump-free ramps between experiments. This makes it possible to use the dds output to generate a
lock signal for a laser cavity lock.
Note that in this mode using amplitude *and* frequency ramps leads to undefined behavior when switching between
them. As the DRG can only address either amplitude or frequency, parallel ramps of both parameters are ramped
in a simple step by step way.
Usage of the DRG can be avoided by setting the component attribute :code:`use_drg` to False.
Attention:
When using the DRG, this method always advances the timeline by at least 10us, even if the ramp is shorter.
Args:
duration: ramp duration [s]
frequency_start: initial frequency [Hz]
frequency_end: end frequency [Hz]
amplitude_start: initial amplitude [0..1]
amplitude_end: end amplitude [0..1]
ramp_timesteps: time between steps in the ramp [s]
ramp_steps: number of steps the whole ramp should have. This takes precedence over `ramp_timesteps`
"""
RFSource.ramp(
self,
duration,
frequency_start=frequency_start,
frequency_end=frequency_end,
amplitude_start=amplitude_start,
amplitude_end=amplitude_end,
ramp_timestep=ramp_timestep,
ramp_steps=ramp_steps,
)
@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)