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

from artiq.language.core import (kernel, portable, delay_mu, delay, now_mu,
                                 at_mu)
from artiq.language.units import ns, us
from artiq.coredevice import spi2 as spi

SPI_AD53XX_CONFIG = (0*spi.SPI_OFFLINE | 1*spi.SPI_END |
                     0*spi.SPI_INPUT | 0*spi.SPI_CS_POLARITY |
                     0*spi.SPI_CLK_POLARITY | 1*spi.SPI_CLK_PHASE |
                     0*spi.SPI_LSB_FIRST | 0*spi.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, value, op): """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, op): """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, offset_dacs=0x2000, vref=5.): """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 = int(round((1 << 16) * (voltage / (4. * vref)) + offset_dacs * 0x4)) if code < 0x0 or code > 0xffff: raise ValueError("Invalid DAC voltage!") return code
class _DummyTTL: @portable def on(self): pass @portable def off(self): pass
[docs] 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") """ kernel_invariants = {"bus", "ldac", "clr", "chip_select", "div_write", "div_read", "vref", "core"} 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=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(channel=0, op=AD53XX_READ_CONTROL) if ctrl == 0xffff: raise ValueError("DAC not found") if ctrl & 0b10000: raise ValueError("DAC over temperature") 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(channel=0, op=AD53XX_READ_CONTROL) if (ctrl & 0b10111) != 0b00010: raise ValueError("DAC CONTROL readback mismatch") delay(15*us)
[docs] @kernel def read_reg(self, channel=0, op=AD53XX_READ_X1A): """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.SPI_INPUT, 24, self.div_read, self.chip_select) 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) # FIXME: the int32 should not be needed to resolve unification return self.bus.read() & int32(0xffff)
[docs] @kernel def write_offset_dacs_mu(self, value): """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, gain=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, offset=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, voltage): """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, value): """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, voltage): """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(2*self.bus.ref_period_mu) # t13 = 10ns ldac pulse width low self.ldac.on()
[docs] @kernel def set_dac_mu(self, values, channels=list(range(40))): """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-len(values)*self.bus.xfer_duration_mu) for i in range(len(values)): self.write_dac_mu(channels[i], values[i]) delay_mu(t_10) self.load() at_mu(t0)
[docs] @kernel def set_dac(self, voltages, channels=list(range(40))): """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, vzs, vfs): """ 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): """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)