diff --git a/artiq/coredevice/ad5360.py b/artiq/coredevice/ad5360.py deleted file mode 100644 index 3cff52d6c..000000000 --- a/artiq/coredevice/ad5360.py +++ /dev/null @@ -1,190 +0,0 @@ -""" -Driver for the AD5360 DAC on RTIO. - -Output event replacement is not supported and issuing commands at the same -time is an error. -""" - - -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 - -# Designed from the data sheets and somewhat after the linux kernel -# iio driver. - -_AD5360_SPI_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) - -_AD5360_CMD_DATA = 3 << 22 -_AD5360_CMD_OFFSET = 2 << 22 -_AD5360_CMD_GAIN = 1 << 22 -_AD5360_CMD_SPECIAL = 0 << 22 - - -@portable -def _AD5360_WRITE_CHANNEL(c): - return (c + 8) << 16 - - -_AD5360_SPECIAL_NOP = 0 << 16 -_AD5360_SPECIAL_CONTROL = 1 << 16 -_AD5360_SPECIAL_OFS0 = 2 << 16 -_AD5360_SPECIAL_OFS1 = 3 << 16 -_AD5360_SPECIAL_READ = 5 << 16 - - -@portable -def _AD5360_READ_CHANNEL(ch): - return (ch + 8) << 7 - - -_AD5360_READ_X1A = 0x000 << 7 -_AD5360_READ_X1B = 0x040 << 7 -_AD5360_READ_OFFSET = 0x080 << 7 -_AD5360_READ_GAIN = 0x0c0 << 7 -_AD5360_READ_CONTROL = 0x101 << 7 -_AD5360_READ_OFS0 = 0x102 << 7 -_AD5360_READ_OFS1 = 0x103 << 7 - - -class AD5360: - """ - Support for the Analog devices AD53[67][0123] - multi-channel Digital to Analog Converters - - :param spi_device: Name of the SPI bus this device is on. - :param ldac_device: Name of the TTL device that LDAC is connected to - (optional). Needs to be explicitly initialized to high. - :param chip_select: Value to drive on the chip select lines - during transactions. - :param div_write: SPI clock divider during writes - :param div_read: SPI clock divider during reads - """ - kernel_invariants = {"bus", "core", "chip_select", "div_read", "div_write"} - - def __init__(self, dmgr, spi_device, ldac_device=None, chip_select=1, - div_write=4, div_read=7): - self.core = dmgr.get("core") - self.bus = dmgr.get(spi_device) - if ldac_device is not None: - self.ldac = dmgr.get(ldac_device) - self.chip_select = chip_select - # write: 2*8ns >= 10ns = t_6 (clk falling to cs_n rising) - # 4*8ns >= 20ns = t_1 (clk cycle time) - self.div_write = div_write - # read: 4*8*ns >= 25ns = t_22 (clk falling to miso valid) - self.div_read = div_read - - @kernel - def setup_bus(self): - """Configure the SPI bus and the SPI transaction parameters - for this device. This method has to be called before any other method - if the bus has been used to access a different device in the meantime. - - This method advances the timeline by one coarse RTIO cycle. - """ - self.bus.set_config_mu(_AD5360_SPI_CONFIG, 24, self.div_write, - self.chip_select) - - @kernel - def write(self, data): - """Write 24 bits of data. - - This method advances the timeline by the duration of the SPI transfer - and the required CS high time. - """ - self.bus.write(data << 8) - delay_mu(self.bus.ref_period_mu) # get to 20ns min cs high - - @kernel - def write_offsets(self, value=0x1fff): - """Write the OFS0 and OFS1 offset DACs. - - This method advances the timeline by twice the duration of - :meth:`write`. - - :param value: Value to set both offset registers to. - """ - value &= 0x3fff - self.write(_AD5360_CMD_SPECIAL | _AD5360_SPECIAL_OFS0 | value) - self.write(_AD5360_CMD_SPECIAL | _AD5360_SPECIAL_OFS1 | value) - - @kernel - def write_channel(self, channel=0, value=0, op=_AD5360_CMD_DATA): - """Write to a channel register. - - This method advances the timeline by the duration of :meth:`write`. - - :param channel: Channel number to write to. - :param value: 16 bit value to write to the register. - :param op: Operation to perform, one of :const:`_AD5360_CMD_DATA`, - :const:`_AD5360_CMD_OFFSET`, :const:`_AD5360_CMD_GAIN` - (default: :const:`_AD5360_CMD_DATA`). - """ - channel &= 0x3f - value &= 0xffff - self.write(op | _AD5360_WRITE_CHANNEL(channel) | value) - - @kernel - def read_channel_sync(self, channel=0, op=_AD5360_READ_X1A): - """Read a channel register. - - This method advances the timeline by the duration of two :meth:`write` - plus two coarse RTIO cycles. - - :param channel: Channel number to read from. - :param op: Operation to perform, one of :const:`_AD5360_READ_X1A`, - :const:`_AD5360_READ_X1B`, :const:`_AD5360_READ_OFFSET`, - :const:`_AD5360_READ_GAIN` (default: :const:`_AD5360_READ_X1A`). - :return: The 16 bit register value. - """ - channel &= 0x3f - self.write(_AD5360_CMD_SPECIAL | _AD5360_SPECIAL_READ | op | - _AD5360_READ_CHANNEL(channel)) - self.bus.set_config_mu(_AD5360_SPI_CONFIG | spi.SPI_INPUT, 24, - self.div_read, self.chip_select) - delay(270*ns) # t_21 min sync high in readback - self.write(_AD5360_CMD_SPECIAL | _AD5360_SPECIAL_NOP) - self.bus.set_config_mu(_AD5360_SPI_CONFIG, 24, - self.div_write, self.chip_select) - return self.bus.read() & 0xffff - - @kernel - def load(self): - """Pulse the LDAC line. - - This method advances the timeline by two RTIO clock periods (16 ns). - """ - self.ldac.off() - # t13 = 10ns ldac pulse width low - delay_mu(2*self.bus.ref_period_mu) - self.ldac.on() - - @kernel - def set(self, values, op=_AD5360_CMD_DATA): - """Write to several channels and pulse LDAC to update the channels. - - This method does not advance the timeline. Write events are scheduled - in the past. The DACs will synchronously start changing their output - levels `now`. - - :param values: List of 16 bit values to write to the channels. - :param op: Operation to perform, one of :const:`_AD5360_CMD_DATA`, - :const:`_AD5360_CMD_OFFSET`, :const:`_AD5360_CMD_GAIN` - (default: :const:`_AD5360_CMD_DATA`). - """ - t0 = now_mu() - # t10 max busy low for one channel - t_10 = self.core.seconds_to_mu(1.5*us) - # compensate all delays that will be applied - delay_mu(-t_10-len(values)*( - self.bus.ref_period_mu + self.bus.xfer_duration_mu)) - for i in range(len(values)): - self.write_channel(i, values[i], op) - delay_mu(t_10) - self.load() - at_mu(t0) diff --git a/artiq/coredevice/ad53xx.py b/artiq/coredevice/ad53xx.py new file mode 100644 index 000000000..65443754d --- /dev/null +++ b/artiq/coredevice/ad53xx.py @@ -0,0 +1,328 @@ +""""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, aligned as the 24 MSB of + a 32-bit integer, ready to be transferred directly by the SPI core. + """ + return (op | ((channel & 0x3f) + 8) << 16 | (value & 0xffff)) << 8 + + +@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, aligned as the 24 MSB of + a 32-bit integer, ready to be transferred directly by the SPI core. + """ + return (AD53XX_CMD_SPECIAL | AD53XX_SPECIAL_READ | op | + (((channel & 0x3f) + 8) << 7)) << 8 + + +@portable +def voltage_to_mu(voltage, offset_dacs=8192, vref=5.): + """Returns the DAC register value required to produce a given output + voltage, assuming offset and gain errors have been trimmed out. + + :param voltage: Voltage + :param offset_dacs: Register value for the two offset DACs (default + :0x1555) + :param vref: DAC reference voltage (default: 5.) + """ + return int(round(0x10000*(voltage/(4.*vref)) + offset_dacs*0x4)) + +@portable +def offset_to_mu(voltage, offset_dacs=8192, vref=5.): + """Returns the 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: Offset voltage + :param offset_dacs: Register value for the two offset DACs (default + :0x1555) + :param vref: DAC reference voltage (default: 5.) + """ + return int(round(0x10000*(voltage/(4.*vref)) + offset_dacs*0x4)) + +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 + :param clr_device: CLR RTIO TTLOut channel name + :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, clr_device, + chip_select=1, div_write=4, div_read=8, vref=5., + offset_dacs=8192, core="core"): + self.bus = dmgr.get(spi_device) + self.ldac = dmgr.get(ldac_device) + 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)) + 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)) + + @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)) + + @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, offset_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)) + + @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`. + + 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`. + + :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) diff --git a/artiq/coredevice/zotino.py b/artiq/coredevice/zotino.py new file mode 100644 index 000000000..dfb5096e1 --- /dev/null +++ b/artiq/coredevice/zotino.py @@ -0,0 +1,53 @@ +"""RTIO driver for the Zotino 32-channel, 16-bit 1MSPS DAC. + +Output event replacement is not supported and issuing commands at the same +time is an error. +""" + +from artiq.language.core import kernel +from artiq.coredevice import spi2 as spi +from artiq.coredevice.ad53xx import SPI_AD53XX_CONFIG, AD53xx + +_SPI_DAC_CONFIG = SPI_AD53XX_CONFIG +_SPI_SR_CONFIG = (0*spi.SPI_OFFLINE | 1*spi.SPI_END | + 0*spi.SPI_INPUT | 0*spi.SPI_CS_POLARITY | + 0*spi.SPI_CLK_POLARITY | 0*spi.SPI_CLK_PHASE | + 0*spi.SPI_LSB_FIRST | 0*spi.SPI_HALF_DUPLEX) + +_SPI_CS_DAC = 1 +_SPI_CS_SR = 2 + +class Zotino(AD53xx): + """ Zotino 32-channel, 16-bit 1MSPS DAC. + + Controls the AD5372 DAC and the 8 user LEDs via a shared SPI interface. + + :param spi_device: SPI bus device name + :param ldac_device: LDAC RTIO TTLOut channel name. + :param clr_device: CLR RTIO TTLOut channel name. + :param div_write: SPI clock divider for write operations (default: 4, + 50MHz max SPI clock) + :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 core_device: Core device name (default: "core") + """ + + def __init__(self, dmgr, spi_device, ldac_device, clr_device, + div_write=4, div_read=8, vref=5., core="core"): + AD53xx.__init__(self, dmgr=dmgr, spi_device=spi_device, + ldac_device=ldac_device, clr_device=clr_device, + chip_select=_SPI_CS_DAC, div_write=div_write, + div_read=div_read, core=core) + + @ kernel + def set_leds(self, leds): + """ Sets the states of the 8 user LEDs. + + :param leds: 8-bit word with LED state + """ + self.bus.set_config_mu(_SPI_SR_CONFIG, 8, self.div_write, _SPI_CS_SR) + self.bus.write(leds << 24) + self.bus.set_config_mu(_SPI_DAC_CONFIG, 24, self.div_write, + _SPI_CS_DAC) diff --git a/artiq/examples/kasli_opticlock/device_db.py b/artiq/examples/kasli_opticlock/device_db.py index 3ee94fb2e..a64c28a36 100644 --- a/artiq/examples/kasli_opticlock/device_db.py +++ b/artiq/examples/kasli_opticlock/device_db.py @@ -198,15 +198,6 @@ device_db = { "class": "TTLOut", "arguments": {"channel": 25} }, - "novogorny0" : { - "type": "local", - "module": "artiq.coredevice.novogorny", - "class": "Novogorny", - "arguments": { - "spi_device": "spi_novogorny0", - "cnv_device": "ttl_novogorny0_cnv", - } - }, "spi_urukul0": { "type": "local", @@ -311,5 +302,34 @@ device_db = { "module": "artiq.coredevice.ttl", "class": "TTLOut", "arguments": {"channel": 33} + }, + + "spi_zotino0": { + "type": "local", + "module": "artiq.coredevice.spi2", + "class": "SPIMaster", + "arguments": {"channel": 36} + }, + "ttl_zotino0_ldac": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 37} + }, + "ttl_zotino0_clr": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 38} + }, + "zotino0": { + "type": "local", + "module": "artiq.coredevice.zotino", + "class": "Zotino", + "arguments": { + "spi_device": "spi_zotino0", + "ldac_device": "ttl_zotino0_ldac", + "clr_device": "ttl_zotino0_clr" + } } }