Source code for artiq.coredevice.ad53xx

"""RTIO driver for the Analog Devices AD53[67][0123] family of multi-channel
Digital to Analog Converters.

Output event replacement is not supported and issuing commands at the same
time results in a collision error.
"""

# Designed from the data sheets and somewhat after the linux kernel
# iio driver.

from numpy import int32, int64

from artiq.language.core import *
from artiq.language.units import ns, us
from artiq.coredevice.core import Core
from artiq.coredevice.ttl import TTLOut
from artiq.coredevice.spi2 import *


SPI_AD53XX_CONFIG = (0*SPI_OFFLINE | 1*SPI_END |
                     0*SPI_INPUT | 0*SPI_CS_POLARITY |
                     0*SPI_CLK_POLARITY | 1*SPI_CLK_PHASE |
                     0*SPI_LSB_FIRST | 0*SPI_HALF_DUPLEX)

AD53XX_CMD_DATA = 3 << 22
AD53XX_CMD_OFFSET = 2 << 22
AD53XX_CMD_GAIN = 1 << 22
AD53XX_CMD_SPECIAL = 0 << 22

AD53XX_SPECIAL_NOP = 0 << 16
AD53XX_SPECIAL_CONTROL = 1 << 16
AD53XX_SPECIAL_OFS0 = 2 << 16
AD53XX_SPECIAL_OFS1 = 3 << 16
AD53XX_SPECIAL_READ = 5 << 16
AD53XX_SPECIAL_AB0 = 6 << 16
AD53XX_SPECIAL_AB1 = 7 << 16
AD53XX_SPECIAL_AB2 = 8 << 16
AD53XX_SPECIAL_AB3 = 9 << 16
AD53XX_SPECIAL_AB = 11 << 16

# incorporate the channel offset (8, table 17) here
AD53XX_READ_X1A = 0x008 << 7
AD53XX_READ_X1B = 0x048 << 7
AD53XX_READ_OFFSET = 0x088 << 7
AD53XX_READ_GAIN = 0x0C8 << 7

AD53XX_READ_CONTROL = 0x101 << 7
AD53XX_READ_OFS0 = 0x102 << 7
AD53XX_READ_OFS1 = 0x103 << 7
AD53XX_READ_AB0 = 0x106 << 7
AD53XX_READ_AB1 = 0x107 << 7
AD53XX_READ_AB2 = 0x108 << 7
AD53XX_READ_AB3 = 0x109 << 7


[docs] @portable def ad53xx_cmd_write_ch(channel: int32, value: int32, op: int32) -> int32: """Returns the word that must be written to the DAC to set a DAC channel register to a given value. :param channel: DAC channel to write to (8 bits) :param value: 16-bit value to write to the register :param op: The channel register to write to, one of :const:`AD53XX_CMD_DATA`, :const:`AD53XX_CMD_OFFSET` or :const:`AD53XX_CMD_GAIN`. :return: The 24-bit word to be written to the DAC """ return op | (channel + 8) << 16 | (value & 0xffff)
[docs] @portable def ad53xx_cmd_read_ch(channel: int32, op: int32) -> int32: """Returns the word that must be written to the DAC to read a given DAC channel register. :param channel: DAC channel to read (8 bits) :param op: The channel register to read, one of :const:`AD53XX_READ_X1A`, :const:`AD53XX_READ_X1B`, :const:`AD53XX_READ_OFFSET`, :const:`AD53XX_READ_GAIN` etc. :return: The 24-bit word to be written to the DAC to initiate read """ return AD53XX_CMD_SPECIAL | AD53XX_SPECIAL_READ | (op + (channel << 7))
# maintain function definition for backward compatibility
[docs] @portable def voltage_to_mu(voltage: float, offset_dacs: int32 = 0x2000, vref: float = 5.) -> int32: """Returns the 16-bit DAC register value required to produce a given output voltage, assuming offset and gain errors have been trimmed out. The 16-bit register value may also be used with 14-bit DACs. The additional bits are disregarded by 14-bit DACs. Also used to return offset register value required to produce a given voltage when the DAC register is set to mid-scale. An offset of V can be used to trim out a DAC offset error of -V. :param voltage: Voltage in SI units. Valid voltages are: [-2*vref, + 2*vref - 1 LSB] + voltage offset. :param offset_dacs: Register value for the two offset DACs (default: 0x2000) :param vref: DAC reference voltage (default: 5.) :return: The 16-bit DAC register value """ code = round(float(1 << 16) * (voltage / (4. * vref))) + offset_dacs * 0x4 if code < 0x0 or code > 0xffff: raise ValueError("Invalid DAC voltage!") return code
@compile class _DummyTTL: @kernel def on(self): pass @kernel def off(self): pass
[docs] @compile class AD53xx: """Analog devices AD53[67][0123] family of multi-channel Digital to Analog Converters. :param spi_device: SPI bus device name :param ldac_device: LDAC RTIO TTLOut channel name (optional) :param clr_device: CLR RTIO TTLOut channel name (optional) :param chip_select: Value to drive on SPI chip select lines during transactions (default: 1) :param div_write: SPI clock divider for write operations (default: 4, 50MHz max SPI clock with {t_high, t_low} >=8ns) :param div_read: SPI clock divider for read operations (default: 16, not optimized for speed; datasheet says t22: 25ns min SCLK edge to SDO valid, and suggests the SPI speed for reads should be <=20 MHz) :param vref: DAC reference voltage (default: 5.) :param offset_dacs: Initial register value for the two offset DACs (default: 8192). Device dependent and must be set correctly for correct voltage-to-mu conversions. Knowledge of this state is not transferred between experiments. :param core_device: Core device name (default: "core") """ core: KernelInvariant[Core] bus: KernelInvariant[SPIMaster] ldac: KernelInvariant[TTLOut] clr: KernelInvariant[TTLOut] chip_select: KernelInvariant[int32] div_write: KernelInvariant[int32] div_read: KernelInvariant[int32] vref: KernelInvariant[float] offset_dacs: Kernel[int32] def __init__(self, dmgr, spi_device, ldac_device=None, clr_device=None, chip_select=1, div_write=4, div_read=16, vref=5., offset_dacs=8192, core="core"): self.bus = dmgr.get(spi_device) self.bus.update_xfer_duration_mu(div_write, 24) if ldac_device is None: self.ldac = _DummyTTL() else: self.ldac = dmgr.get(ldac_device) if clr_device is None: self.clr = _DummyTTL() else: self.clr = dmgr.get(clr_device) self.chip_select = chip_select self.div_write = div_write self.div_read = div_read self.vref = vref self.offset_dacs = offset_dacs self.core = dmgr.get(core)
[docs] @kernel def init(self, blind: bool = False): """Configures the SPI bus, drives LDAC and CLR high, programmes the offset DACs, and enables overtemperature shutdown. This method must be called before any other method at start-up or if the SPI bus has been accessed by another device. :param blind: If ``True``, do not attempt to read back control register or check for overtemperature. """ self.ldac.on() self.clr.on() self.bus.set_config_mu(SPI_AD53XX_CONFIG, 24, self.div_write, self.chip_select) self.write_offset_dacs_mu(self.offset_dacs) if not blind: ctrl = self.read_reg(0, AD53XX_READ_CONTROL) if ctrl == 0xffff: raise ValueError("DAC not found") if (ctrl & 0b10000) != 0: raise ValueError("DAC over temperature") self.core.delay(25.*us) self.bus.write( # enable power and overtemperature shutdown (AD53XX_CMD_SPECIAL | AD53XX_SPECIAL_CONTROL | 0b0010) << 8) if not blind: ctrl = self.read_reg(0, AD53XX_READ_CONTROL) if (ctrl & 0b10111) != 0b00010: raise ValueError("DAC CONTROL readback mismatch") self.core.delay(15.*us)
[docs] @kernel def read_reg(self, channel: int32 = 0, op: int32 = AD53XX_READ_X1A) -> int32: """Read a DAC register. This method advances the timeline by the duration of two SPI transfers plus two RTIO coarse cycles plus 270 ns and consumes all slack. :param channel: Channel number to read from (default: 0) :param op: Operation to perform, one of :const:`AD53XX_READ_X1A`, :const:`AD53XX_READ_X1B`, :const:`AD53XX_READ_OFFSET`, :const:`AD53XX_READ_GAIN` etc. (default: :const:`AD53XX_READ_X1A`). :return: The 16-bit register value """ self.bus.write(ad53xx_cmd_read_ch(channel, op) << 8) self.bus.set_config_mu(SPI_AD53XX_CONFIG | SPI_INPUT, 24, self.div_read, self.chip_select) self.core.delay(270.*ns) # t_21 min sync high in readback self.bus.write((AD53XX_CMD_SPECIAL | AD53XX_SPECIAL_NOP) << 8) self.bus.set_config_mu(SPI_AD53XX_CONFIG, 24, self.div_write, self.chip_select) return self.bus.read() & 0xffff
[docs] @kernel def write_offset_dacs_mu(self, value: int32): """Program the OFS0 and OFS1 offset DAC registers. Writes to the offset DACs take effect immediately without requiring a LDAC. This method advances the timeline by the duration of two SPI transfers. :param value: Value to set both offset DAC registers to """ value &= 0x3fff self.offset_dacs = value self.bus.write((AD53XX_CMD_SPECIAL | AD53XX_SPECIAL_OFS0 | value) << 8) self.bus.write((AD53XX_CMD_SPECIAL | AD53XX_SPECIAL_OFS1 | value) << 8)
[docs] @kernel def write_gain_mu(self, channel: int32, gain: int32 = 0xffff): """Program the gain register for a DAC channel. The DAC output is not updated until LDAC is pulsed (see :meth:`load`). This method advances the timeline by the duration of one SPI transfer. :param gain: 16-bit gain register value (default: 0xffff) """ self.bus.write( ad53xx_cmd_write_ch(channel, gain, AD53XX_CMD_GAIN) << 8)
[docs] @kernel def write_offset_mu(self, channel: int32, offset: int32 = 0x8000): """Program the offset register for a DAC channel. The DAC output is not updated until LDAC is pulsed (see :meth:`load`). This method advances the timeline by the duration of one SPI transfer. :param offset: 16-bit offset register value (default: 0x8000) """ self.bus.write( ad53xx_cmd_write_ch(channel, offset, AD53XX_CMD_OFFSET) << 8)
[docs] @kernel def write_offset(self, channel: int32, voltage: float): """Program the DAC offset voltage for a channel. An offset of +V can be used to trim out a DAC offset error of -V. The DAC output is not updated until LDAC is pulsed (see :meth:`load`). This method advances the timeline by the duration of one SPI transfer. :param voltage: the offset voltage """ self.write_offset_mu(channel, voltage_to_mu(voltage, self.offset_dacs, self.vref))
[docs] @kernel def write_dac_mu(self, channel: int32, value: int32): """Program the DAC input register for a channel. The DAC output is not updated until LDAC is pulsed (see :meth:`load`). This method advances the timeline by the duration of one SPI transfer. """ self.bus.write( ad53xx_cmd_write_ch(channel, value, AD53XX_CMD_DATA) << 8)
[docs] @kernel def write_dac(self, channel: int32, voltage: float): """Program the DAC output voltage for a channel. The DAC output is not updated until LDAC is pulsed (see :meth:`load`). This method advances the timeline by the duration of one SPI transfer. """ self.write_dac_mu(channel, voltage_to_mu(voltage, self.offset_dacs, self.vref))
[docs] @kernel def load(self): """Pulse the LDAC line. Note that there is a <= 1.5us "BUSY" period (t10) after writing to a DAC input/gain/offset register. All DAC registers may be programmed normally during the busy period, however LDACs during the busy period cause the DAC output to change *after* the BUSY period has completed, instead of the usual immediate update on LDAC behaviour. This method advances the timeline by two RTIO clock periods. """ self.ldac.off() delay_mu(int64(2)*self.bus.ref_period_mu) # t13 = 10ns ldac pulse width low self.ldac.on()
[docs] @kernel def set_dac_mu(self, values: list[int32], channels: Option[list[int32]] = none): """Program multiple DAC channels and pulse LDAC to update the DAC outputs. This method does not advance the timeline; write events are scheduled in the past. The DACs will synchronously start changing their output levels ``now``. If no LDAC device was defined, the LDAC pulse is skipped. See :meth:`load`. :param values: list of DAC values to program :param channels: list of DAC channels to program. If not specified, we program the DAC channels sequentially, starting at 0. """ t0 = now_mu() # t10: max busy period after writing to DAC registers t_10 = self.core.seconds_to_mu(1500.*ns) # compensate all delays that will be applied delay_mu(-t_10-int64(len(values))*self.bus.xfer_duration_mu) channels_list = channels.unwrap() if channels.is_some() else [i for i in range(40)] for i in range(len(values)): self.write_dac_mu(channels_list[i], values[i]) delay_mu(t_10) self.load() at_mu(t0)
[docs] @kernel def set_dac(self, voltages: list[float], channels: Option[list[int32]] = none): """Program multiple DAC channels and pulse LDAC to update the DAC outputs. This method does not advance the timeline; write events are scheduled in the past. The DACs will synchronously start changing their output levels `now`. If no LDAC device was defined, the LDAC pulse is skipped. :param voltages: list of voltages to program the DAC channels to :param channels: list of DAC channels to program. If not specified, we program the DAC channels sequentially, starting at 0. """ values = [voltage_to_mu(voltage, self.offset_dacs, self.vref) for voltage in voltages] self.set_dac_mu(values, channels)
[docs] @kernel def calibrate(self, channel: int32, vzs: float, vfs: float): """Two-point calibration of a DAC channel. Programs the offset and gain register to trim out DAC errors. Does not take effect until LDAC is pulsed (see :meth:`load`). Calibration consists of measuring the DAC output voltage for a channel with the DAC set to zero-scale (0x0000) and full-scale (0xffff). Note that only negative offsets and full-scale errors (DAC gain too high) can be calibrated in this fashion. :param channel: The number of the calibrated channel :param vzs: Measured voltage with the DAC set to zero-scale (0x0000) :param vfs: Measured voltage with the DAC set to full-scale (0xffff) """ offset_err = voltage_to_mu(vzs, self.offset_dacs, self.vref) gain_err = voltage_to_mu(vfs, self.offset_dacs, self.vref) - ( offset_err + 0xffff) assert offset_err <= 0 assert gain_err >= 0 self.core.break_realtime() self.write_offset_mu(channel, 0x8000-offset_err) self.write_gain_mu(channel, 0xffff-gain_err)
[docs] @portable def voltage_to_mu(self, voltage: float) -> int32: """Returns the 16-bit DAC register value required to produce a given output voltage, assuming offset and gain errors have been trimmed out. The 16-bit register value may also be used with 14-bit DACs. The additional bits are disregarded by 14-bit DACs. :param voltage: Voltage in SI units. Valid voltages are: [-2*vref, + 2*vref - 1 LSB] + voltage offset. :return: The 16-bit DAC register value """ return voltage_to_mu(voltage, self.offset_dacs, self.vref)