Source code for atomiq.components.sinara.urukul

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)