diff --git a/artiq/coredevice/shuttler.py b/artiq/coredevice/shuttler.py index 358522eac..722b95f2a 100644 --- a/artiq/coredevice/shuttler.py +++ b/artiq/coredevice/shuttler.py @@ -9,10 +9,27 @@ from artiq.language.units import us @portable def shuttler_volt_to_mu(volt): + """Return the equivalent DAC code. Valid input range is from -10 to + 10 - LSB. + """ return round((1 << 14) * (volt / 20.0)) & 0x3fff class Config: + """Shuttler configuration registers interface. + + The configuration registers control waveform phase auto-clear, and pre-DAC + gain & offset values for calibration with ADC on the Shuttler AFE card. + + To find the calibrated DAC code, the Shuttler core first multiplies the + output data with pre-DAC gain, then adds the offset. + + .. note:: + The DAC code is capped at 0x1fff and 0x2000. + + :param channel: RTIO channel number of this interface. + :param core_device: Core device name. + """ kernel_invariants = { "core", "channel", "target_base", "target_read", "target_gain", "target_offset", "target_clr" @@ -29,30 +46,87 @@ class Config: @kernel def set_clr(self, clr): + """Set/Unset waveform phase clear bits. + + Each bit corresponds to a Shuttler waveform generator core. Setting a + clear bit forces the Shuttler core to clear the phase accumulator on + waveform trigger (See :class:`Trigger` for the trigger method). + Otherwise, the phase accumulator increments from its original value. + + :param clr: Waveform phase clear bits. The MSB corresponds to Channel + 15, LSB corresponds to Channel 0. + """ rtio_output(self.target_base | self.target_clr, clr) @kernel def set_gain(self, channel, gain): + """Set the 16-bits pre-DAC gain register of a Shuttler Core channel. + + The `gain` parameter represents the decimal portion of the gain + factor. The MSB represents 0.5 and the sign bit. Hence, the valid + total gain value (1 +/- 0.gain) ranges from 0.5 to 1.5 - LSB. + + :param channel: Shuttler Core channel to be configured. + :param gain: Shuttler Core channel gain. + """ rtio_output(self.target_base | self.target_gain | channel, gain) @kernel def get_gain(self, channel): + """Return the pre-DAC gain value of a Shuttler Core channel. + + :param channel: The Shuttler Core channel. + :return: Pre-DAC gain value. See :meth:`set_gain`. + """ rtio_output(self.target_base | self.target_gain | self.target_read | channel, 0) return rtio_input_data(self.channel) @kernel def set_offset(self, channel, offset): + """Set the 16-bits pre-DAC offset register of a Shuttler Core channel. + + .. seealso:: + :meth:`shuttler_volt_to_mu` + + :param channel: Shuttler Core channel to be configured. + :param offset: Shuttler Core channel offset. + """ rtio_output(self.target_base | self.target_offset | channel, offset) @kernel def get_offset(self, channel): + """Return the pre-DAC offset value of a Shuttler Core channel. + + :param channel: The Shuttler Core channel. + :return: Pre-DAC offset value. See :meth:`set_offset`. + """ rtio_output(self.target_base | self.target_offset | self.target_read | channel, 0) return rtio_input_data(self.channel) class Volt: + """Shuttler Core cubic DC-bias spline. + + A Shuttler channel can generate a waveform `w(t)` that is the sum of a + cubic spline `a(t)` and a sinusoid modulated in amplitude by a cubic + spline `b(t)` and in phase/frequency by a quadratic spline `c(t)`, where + + .. math:: + w(t) = a(t) + b(t) * cos(c(t)) + + And `t` corresponds to time in seconds. + This class controls the cubic spline `a(t)`, in which + + .. math:: + a(t) = p_0 + p_1t + \\frac{p_2t^2}{2} + \\frac{p_3t^3}{6} + + And `a(t)` is in Volt. + + :param channel: RTIO channel number of this DC-bias spline interface. + :param core_device: Core device name. + """ kernel_invariants = {"core", "channel", "target_o"} def __init__(self, dmgr, channel, core_device="core"): @@ -62,6 +136,34 @@ class Volt: @kernel def set_waveform(self, a0: TInt32, a1: TInt32, a2: TInt64, a3: TInt64): + """Set the DC-bias spline waveform. + + Given `a(t)` as defined in :class:`Volt`, the coefficients should be + configured by the following formulae. + + .. math:: + T &= 8*10^{-9} + + a_0 &= p_0 + + a_1 &= p_1T + \\frac{p_2T^2}{2} + \\frac{p_3T^3}{6} + + a_2 &= p_2T^2 + p_3T^3 + + a_3 &= p_3T^3 + + :math:`a_0`, :math:`a_1`, :math:`a_2` and :math:`a_3` are 14, 30, 46 + and 46 bits in width respectively. See :meth:`shuttler_volt_to_mu` for + machine unit conversion. + + Note: The waveform is not updated to the Shuttler core until + triggered. See :class:`Trigger` for the update triggering mechanism. + + :param a0: The :math:`a_0` coefficient in machine unit. + :param a1: The :math:`a_1` coefficient in machine unit. + :param a2: The :math:`a_2` coefficient in machine unit. + :param a3: The :math:`a_3` coefficient in machine unit. + """ pdq_words = [ a0, a1, @@ -80,6 +182,30 @@ class Volt: class Dds: + """Shuttler Core DDS spline. + + A Shuttler channel can generate a waveform `w(t)` that is the sum of a + cubic spline `a(t)` and a sinusoid modulated in amplitude by a cubic + spline `b(t)` and in phase/frequency by a quadratic spline `c(t)`, where + + .. math:: + w(t) = a(t) + b(t) * cos(c(t)) + + And `t` corresponds to time in seconds. + This class controls the cubic spline `b(t)` and quadratic spline `c(t)`, + in which + + .. math:: + b(t) &= g * (q_0 + q_1t + \\frac{q_2t^2}{2} + \\frac{q_3t^3}{6}) + + c(t) &= r_0 + r_1t + \\frac{r_2t^2}{2} + + And `b(t)` is in Volt, `c(t)` is in number of turns. Note that `b(t)` + contributes to a constant gain of :math:`g=1.64676`. + + :param channel: RTIO channel number of this DC-bias spline interface. + :param core_device: Core device name. + """ kernel_invariants = {"core", "channel", "target_o"} def __init__(self, dmgr, channel, core_device="core"): @@ -90,6 +216,44 @@ class Dds: @kernel def set_waveform(self, b0: TInt32, b1: TInt32, b2: TInt64, b3: TInt64, c0: TInt32, c1: TInt32, c2: TInt32): + """Set the DDS spline waveform. + + Given `b(t)` and `c(t)` as defined in :class:`Dds`, the coefficients + should be configured by the following formulae. + + .. math:: + T &= 8*10^{-9} + + b_0 &= q_0 + + b_1 &= q_1T + \\frac{q_2T^2}{2} + \\frac{q_3T^3}{6} + + b_2 &= q_2T^2 + q_3T^3 + + b_3 &= q_3T^3 + + c_0 &= r_0 + + c_1 &= r_1T + \\frac{r_2T^2}{2} + + c_2 &= r_2T^2 + + :math:`b_0`, :math:`b_1`, :math:`b_2` and :math:`b_3` are 14, 30, 46 + and 46 bits in width respectively. See :meth:`shuttler_volt_to_mu` for + machine unit conversion. :math:`c_0`, :math:`c_1` and :math:`c_2` are + 16, 32 and 32 bits in width respectively. + + Note: The waveform is not updated to the Shuttler core until + triggered. See :class:`Trigger` for the update triggering mechanism. + + :param b0: The :math:`b_0` coefficient in machine unit. + :param b1: The :math:`b_1` coefficient in machine unit. + :param b2: The :math:`b_2` coefficient in machine unit. + :param b3: The :math:`b_3` coefficient in machine unit. + :param c0: The :math:`c_0` coefficient in machine unit. + :param c1: The :math:`c_1` coefficient in machine unit. + :param c2: The :math:`c_2` coefficient in machine unit. + """ pdq_words = [ b0, b1, @@ -113,6 +277,11 @@ class Dds: class Trigger: + """Shuttler Core spline coefficients update trigger. + + :param channel: RTIO channel number of the trigger interface. + :param core_device: Core device name. + """ kernel_invariants = {"core", "channel", "target_o"} def __init__(self, dmgr, channel, core_device="core"): @@ -122,6 +291,16 @@ class Trigger: @kernel def trigger(self, trig_out): + """Triggers coefficient update of (a) Shuttler core channel(s). + + Each bit corresponds to a Shuttler waveform generator core. Setting + `trig_out` bits commits the pending coefficient update (from + `set_waveform` in :class:`Volt` and :class:`Dds`) to the Shuttler core + synchronously. + + :param trig_out: Coefficient update trigger bits. The MSB corresponds + to Channel 15, LSB corresponds to Channel 0. + """ rtio_output(self.target_o, trig_out) @@ -157,6 +336,19 @@ _AD4115_REG_SETUPCON0 = 0x20 class Relay: + """Shuttler AFE relay switches. + + It controls the AFE relay switches and the LEDs. Switch on the relay to + enable AFE output; And off to disable the output. The LEDs indicates the + relay status. + + .. note:: + The relay does not disable ADC measurements. Voltage of any channels + can still be read by the ADC even after switching off the relays. + + :param spi_device: SPI bus device name. + :param core_device: Core device name. + """ kernel_invariant = {"core", "bus"} def __init__(self, dmgr, spi_device, core_device="core"): @@ -165,15 +357,34 @@ class Relay: @kernel def init(self): + """Initialize SPI device. + + Configures the SPI bus to 16-bits, write-only, simultaneous relay + switches and LED control. + """ self.bus.set_config_mu( RELAY_SPI_CONFIG, 16, SPIT_RELAY_WR, CS_RELAY | CS_LED) @kernel def enable(self, en: TInt32): + """Enable/Disable relay switches of corresponding channels. + + Each bit corresponds to the relay switch of a channel. Asserting a bit + turns on the corresponding relay switch; Deasserting the same bit + turns off the switch instead. + + :param en: Switch enable bits. The MSB corresponds to Channel 15, LSB + corresponds to Channel 0. + """ self.bus.write(en << 16) class ADC: + """Shuttler AFE ADC (AD4115) driver. + + :param spi_device: SPI bus device name. + :param core_device: Core device name. + """ kernel_invariant = {"core", "bus"} def __init__(self, dmgr, spi_device, core_device="core"): @@ -182,13 +393,26 @@ class ADC: @kernel def read_id(self) -> TInt32: + """Read the product ID of the ADC. + + The expected return value is 0x38DX, the 4 LSbs are don't cares. + + :return: The read-back product ID. + """ return self.read16(_AD4115_REG_ID) @kernel def reset(self): - # Hold DIN high for 64 cycles - # However, asserting CS right after the 64 cycles seems to interrupt - # the start-up sequence. + """AD4115 reset procedure. + + This performs a write operation of 96 serial clock cycles with DIN + held at high. It resets the entire device, including the register + contents. + + .. note:: + The datasheet only requires 64 cycles, but reaserting `CS_n` right + after the transfer appears to interrupt the start-up sequence. + """ self.bus.set_config_mu(ADC_SPI_CONFIG, 32, SPIT_ADC_WR, CS_ADC) self.bus.write(0xffffffff) self.bus.write(0xffffffff) @@ -198,6 +422,11 @@ class ADC: @kernel def read8(self, addr: TInt32) -> TInt32: + """Read from 8 bit register. + + :param addr: Register address. + :return: Read-back register content. + """ self.bus.set_config_mu( ADC_SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, 16, SPIT_ADC_RD, CS_ADC) @@ -206,6 +435,11 @@ class ADC: @kernel def read16(self, addr: TInt32) -> TInt32: + """Read from 16 bit register. + + :param addr: Register address. + :return: Read-back register content. + """ self.bus.set_config_mu( ADC_SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, 24, SPIT_ADC_RD, CS_ADC) @@ -214,6 +448,11 @@ class ADC: @kernel def read24(self, addr: TInt32) -> TInt32: + """Read from 24 bit register. + + :param addr: Register address. + :return: Read-back register content. + """ self.bus.set_config_mu( ADC_SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, 32, SPIT_ADC_RD, CS_ADC) @@ -222,44 +461,102 @@ class ADC: @kernel def write8(self, addr: TInt32, data: TInt32): + """Write to 8 bit register. + + :param addr: Register address. + :param data: Data to be written. + """ self.bus.set_config_mu( ADC_SPI_CONFIG | spi.SPI_END, 16, SPIT_ADC_WR, CS_ADC) self.bus.write(addr << 24 | (data & 0xff) << 16) @kernel def write16(self, addr: TInt32, data: TInt32): + """Write to 16 bit register. + + :param addr: Register address. + :param data: Data to be written. + """ self.bus.set_config_mu( ADC_SPI_CONFIG | spi.SPI_END, 24, SPIT_ADC_WR, CS_ADC) self.bus.write(addr << 24 | (data & 0xffff) << 8) @kernel def write24(self, addr: TInt32, data: TInt32): + """Write to 24 bit register. + + :param addr: Register address. + :param data: Data to be written. + """ self.bus.set_config_mu( ADC_SPI_CONFIG | spi.SPI_END, 32, SPIT_ADC_WR, CS_ADC) self.bus.write(addr << 24 | (data & 0xffffff)) @kernel def read_ch(self, channel: TInt32) -> TFloat: + """Sample a Shuttler channel on the AFE. + + It performs a single conversion using profile 0 and setup 0, on the + selected channel. The sample is then recovered and converted to volt. + + :param channel: Shuttler channel to be sampled. + :return: Voltage sample in volt. + """ # Always configure Profile 0 for single conversion self.write16(_AD4115_REG_CH0, 0x8000 | ((channel * 2 + 1) << 4)) self.write16(_AD4115_REG_SETUPCON0, 0x1300) - self.write16(_AD4115_REG_ADCMODE, 0x8010) + self.single_conversion() delay(100*us) adc_code = self.read24(_AD4115_REG_DATA) return ((adc_code / (1 << 23)) - 1) * 2.5 / 0.1 + + @kernel + def single_conversion(self): + """Place the ADC in single conversion mode. + + The ADC returns to standby mode after the conversion is complete. + """ + self.write16(_AD4115_REG_ADCMODE, 0x8010) @kernel def standby(self): + """Place the ADC in standby mode and disables power down the clock. + + The ADC can be returned to single conversion mode by calling + :meth:`single_conversion`. + """ # Selecting internal XO (0b00) also disables clock during standby self.write16(_AD4115_REG_ADCMODE, 0x8020) @kernel def power_down(self): + """Place the ADC in power-down mode. + + The ADC must be reset before returning to other modes. + + .. note:: + The AD4115 datasheet suggests placing the ADC in standby mode + before power-down. This is to prevent accidental entry into the + power-down mode. + + .. seealso:: + :meth:`standby` + + :meth:`power_up` + + """ self.write16(_AD4115_REG_ADCMODE, 0x8030) @kernel - def exit_power_down(self): + def power_up(self): + """Exit the ADC power-down mode. + + The ADC should be in power-down mode before calling this method. + + .. seealso:: + :meth:`power_down` + """ self.reset() # Although the datasheet claims 500 us reset wait time, only waiting # for ~500 us can result in DOUT pin stuck in high @@ -267,6 +564,30 @@ class ADC: @kernel def calibrate(self, volts, trigger, config, samples=[-5.0, 0.0, 5.0]): + """Calibrate the Shuttler waveform generator using the ADC on the AFE. + + It finds the average slope rate and average offset by samples, and + compensate by writing the pre-DAC gain and offset registers in the + configuration registers. + + .. note:: + If the pre-calibration slope rate < 1, the calibration procedure + will introduce a pre-DAC gain compensation. However, this may + saturate the pre-DAC voltage code. (See :class:`Config` notes). + Shuttler cannot cover the entire +/- 10 V range in this case. + + .. seealso:: + :meth:`Config.set_gain` + + :meth:`Config.set_offset` + + :param volts: A list of all 16 cubic DC-bias spline. + (See :class:`Volt`) + :param trigger: The Shuttler spline coefficient update trigger. + :param config: The Shuttler core configuration registers. + :param samples: A list of sample voltages for calibration. There must + be at least 2 samples to perform slope rate calculation. + """ assert len(volts) == 16 assert len(samples) > 1 diff --git a/doc/manual/core_drivers_reference.rst b/doc/manual/core_drivers_reference.rst index ca81ff2ed..5de40c63f 100644 --- a/doc/manual/core_drivers_reference.rst +++ b/doc/manual/core_drivers_reference.rst @@ -143,6 +143,12 @@ DAC/ADC drivers .. automodule:: artiq.coredevice.fastino :members: +:mod:`artiq.coredevice.shuttler` module +++++++++++++++++++++++++++++++++++++++++ + +.. automodule:: artiq.coredevice.shuttler + :members: + Miscellaneous -------------