forked from M-Labs/artiq
coredevice/zotino: add (#969)
* Replace ad5360 driver with a ad53xx driver, designed to have a nicer interface Add Zotino driver and add to opticlock target for Kasli Test Zotino on hw: - Verify all timings on the hardware with a scope - Verify that we can correctly set and read back all registers in a loop (checks for SI and driver issues) - check we can set LEDs correctly - check calibration routine + all si unit functions with a good DVM - look at DAC transitions on a scope (while triggering of a TTL) on persist to check there are no LDAC glitches etc To do: update examples and e.g. KC705 device db.
This commit is contained in:
parent
1553fc8c7d
commit
a992a672d9
|
@ -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)
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -198,15 +198,6 @@ device_db = {
|
||||||
"class": "TTLOut",
|
"class": "TTLOut",
|
||||||
"arguments": {"channel": 25}
|
"arguments": {"channel": 25}
|
||||||
},
|
},
|
||||||
"novogorny0" : {
|
|
||||||
"type": "local",
|
|
||||||
"module": "artiq.coredevice.novogorny",
|
|
||||||
"class": "Novogorny",
|
|
||||||
"arguments": {
|
|
||||||
"spi_device": "spi_novogorny0",
|
|
||||||
"cnv_device": "ttl_novogorny0_cnv",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"spi_urukul0": {
|
"spi_urukul0": {
|
||||||
"type": "local",
|
"type": "local",
|
||||||
|
@ -311,5 +302,34 @@ device_db = {
|
||||||
"module": "artiq.coredevice.ttl",
|
"module": "artiq.coredevice.ttl",
|
||||||
"class": "TTLOut",
|
"class": "TTLOut",
|
||||||
"arguments": {"channel": 33}
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue