""""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 is an error. """ # Designed from the data sheets and somewhat after the linux kernel # iio driver. from artiq.language.core import (kernel, portable, delay_mu, delay, now_mu, at_mu) from artiq.language.units import ns 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_READ_X1A = 0X000 << 7 AD53XX_READ_X1B = 0X040 << 7 AD53XX_READ_OFFSET = 0X080 << 7 AD53XX_READ_GAIN = 0X0C0 << 7 AD53XX_READ_CONTROL = 0X101 << 7 AD53XX_READ_OFS0 = 0X102 << 7 AD53XX_READ_OFS1 = 0X103 << 7 @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) @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_CMD_DATA`, :const:`AD53XX_CMD_OFFSET` or :const:`AD53XX_CMD_GAIN` :return: The 24-bit word to be written to the DAC """ return (AD53XX_CMD_SPECIAL | AD53XX_SPECIAL_READ | op | ((channel + 8) << 7)) @portable def voltage_to_mu(voltage, offset_dacs=0x2000, vref=5.): """Returns the DAC register value required to produce a given output voltage, assuming offset and gain errors have been trimmed out. 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 :param offset_dacs: Register value for the two offset DACs (default: 0x2000) :param vref: DAC reference voltage (default: 5.) """ return int(round(0x10000*(voltage/(4.*vref)) + offset_dacs*0x4)) class _DummyTTL: @portable def on(self): pass @portable def off(self): pass 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: 8, not optimized for speed, but cf data sheet t22: 25ns min SCLK edge to SDO valid) :param vref: DAC reference voltage (default: 5.) :param offset_dacs: Initial register value for the two offset DACs, device dependent and must be set correctly for correct voltage to mu conversions (default: 8192) :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=8, vref=5., offset_dacs=8192, core="core"): self.bus = dmgr.get(spi_device) 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) @kernel def init(self): """Configures the SPI bus, drives LDAC and CLR high and programmes the offset DACss. This method must be called before any other method at start-up or if the SPI bus has been accessed by another device. This method advances the timeline by one coarse RTIO cycle. """ 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) @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. :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` (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) return self.bus.read() @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) @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) @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) @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)) @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) @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)) @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() @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) @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) @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 :params vzs: Measured voltage with the DAC set to zero-scale (0x0000) :params 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)