diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index 0b035930cb..1488fcda64 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -25,6 +25,15 @@ ARTIQ-9 (Unreleased) * Support for coredevice reflashing through the new ``flash`` tool in ``artiq_coremgmt``. * ``artiq_coremgmt`` now supports configuring satellites. * ``artiq.coredevice.fmcdio_vhdci_eem`` has been removed. +* ``artiq.coredevice.urukul`` and ``artiq.coredevice.ad9910`` have been updated for new Urukul capabilities: + - Digital Ramp Generation (DRG) + - Individual DDS Channel control: + * PROFILE + * IO_UPDATE + * OSK + * DRCTRL + * DRHOLD + * ATT_EN ARTIQ-8 ------- diff --git a/artiq/coredevice/ad9910.py b/artiq/coredevice/ad9910.py index 821411b8eb..3d55dd715d 100644 --- a/artiq/coredevice/ad9910.py +++ b/artiq/coredevice/ad9910.py @@ -1,13 +1,11 @@ from numpy import int32, int64 -from artiq.language.core import ( - kernel, delay, portable, delay_mu, now_mu, at_mu) -from artiq.language.units import us, ms -from artiq.language.types import TBool, TInt32, TInt64, TFloat, TList, TTuple - from artiq.coredevice import spi2 as spi from artiq.coredevice import urukul -from artiq.coredevice.urukul import DEFAULT_PROFILE +from artiq.coredevice.urukul import DEFAULT_PROFILE, _RegIOUpdate +from artiq.language.core import at_mu, delay, delay_mu, kernel, now_mu, portable +from artiq.language.types import TBool, TFloat, TInt32, TInt64, TList, TTuple +from artiq.language.units import ms, us # Work around ARTIQ-Python import machinery urukul_sta_pll_lock = urukul.urukul_sta_pll_lock @@ -174,8 +172,12 @@ def __init__(self, dmgr, chip_select, cpld_device, sw_device=None, self.sysclk_per_mu = int(round(sysclk * self.core.ref_period)) self.sysclk = sysclk - if isinstance(sync_delay_seed, str) or isinstance(io_update_delay, - str): + if not self.cpld.io_update: + self.io_update = _RegIOUpdate(self.cpld, self.chip_select) + else: + self.io_update = self.cpld.io_update + + if isinstance(sync_delay_seed, str) or isinstance(io_update_delay, str): if sync_delay_seed != io_update_delay: raise ValueError("When using EEPROM, sync_delay_seed must be " "equal to io_update_delay") @@ -425,7 +427,10 @@ def set_cfr1(self, @kernel def set_cfr2(self, asf_profile_enable: TInt32 = 1, + drg_destination: TInt32 = 0, drg_enable: TInt32 = 0, + drg_nodwell_high: TInt32 = 0, + drg_nodwell_low: TInt32 = 0, effective_ftw: TInt32 = 1, sync_validation_disable: TInt32 = 0, matched_latency_enable: TInt32 = 0): @@ -434,19 +439,27 @@ def set_cfr2(self, This method does not pulse ``IO_UPDATE``. :param asf_profile_enable: Enable amplitude scale from single tone profiles. + :param drg_destination: Digital ramp destination. Determines the parameter to modulate: + * 0: Frequency + * 1: Phase + * 2: Amplitude :param drg_enable: Digital ramp enable. + :param drg_nodwell_high: Digital ramp no-dwell high. + :param drg_nodwell_low: Digital ramp no-dwell low. :param effective_ftw: Read effective FTW. :param sync_validation_disable: Disable the SYNC_SMP_ERR pin indicating (active high) detection of a synchronization pulse sampling error. - :param matched_latency_enable: Simultaneous application of amplitude, - phase, and frequency changes to the DDS arrive at the output - - * matched_latency_enable = 0: in the order listed - * matched_latency_enable = 1: simultaneously. + :param matched_latency_enable: Control the application timing of amplitude, + phase, and frequency changes at the DDS output: + * 0: Changes are applied in the order listed. + * 1: Changes are applied simultaneously. """ self.write32(_AD9910_REG_CFR2, (asf_profile_enable << 24) | + (drg_destination << 20) | (drg_enable << 19) | + (drg_nodwell_high << 18) | + (drg_nodwell_low << 17) | (effective_ftw << 16) | (matched_latency_enable << 7) | (sync_validation_disable << 5)) @@ -471,7 +484,7 @@ def init(self, blind: TBool = False): # Set SPI mode self.set_cfr1() - self.cpld.io_update.pulse(1 * us) + self.io_update.pulse(1 * ms) delay(1 * ms) if not blind: # Use the AUX DAC setting to identify and confirm presence @@ -484,15 +497,15 @@ def init(self, blind: TBool = False): # read effective FTW # sync timing validation disable (enabled later) self.set_cfr2(sync_validation_disable=1) - self.cpld.io_update.pulse(1 * us) + self.io_update.pulse(1 * ms) cfr3 = (0x0807c000 | (self.pll_vco << 24) | (self.pll_cp << 19) | (self.pll_en << 8) | (self.pll_n << 1)) self.write32(_AD9910_REG_CFR3, cfr3 | 0x400) # PFD reset - self.cpld.io_update.pulse(1 * us) + self.io_update.pulse(1 * us) if self.pll_en: self.write32(_AD9910_REG_CFR3, cfr3) - self.cpld.io_update.pulse(1 * us) + self.io_update.pulse(1 * ms) if blind: delay(100 * ms) else: @@ -509,6 +522,9 @@ def init(self, blind: TBool = False): if self.sync_data.sync_delay_seed >= 0 and not blind: self.tune_sync_delay(self.sync_data.sync_delay_seed) delay(1 * ms) + # FIXME: Re-write the configuration (needed for proper + # initialization when using _RegIOUpdate). + self.cpld.cfg_write(self.cpld.cfg_reg) @kernel def power_down(self, bits: TInt32 = 0b1111): @@ -517,7 +533,7 @@ def power_down(self, bits: TInt32 = 0b1111): :param bits: Power-down bits, see datasheet """ self.set_cfr1(power_down=bits) - self.cpld.io_update.pulse(1 * us) + self.io_update.pulse(1 * us) @kernel def set_mu(self, ftw: TInt32 = 0, pow_: TInt32 = 0, asf: TInt32 = 0x3fff, @@ -585,7 +601,7 @@ def set_mu(self, ftw: TInt32 = 0, pow_: TInt32 = 0, asf: TInt32 = 0x3fff, if not ram_destination == RAM_DEST_POW: self.set_pow(pow_) delay_mu(int64(self.sync_data.io_update_delay)) - self.cpld.io_update.pulse_mu(8) # assumes 8 mu > t_SYN_CCLK + self.io_update.pulse_mu(8) # assumes 8 mu > t_SYN_CCLK at_mu(now_mu() & ~7) # clear fine TSC again if phase_mode != PHASE_MODE_CONTINUOUS: self.set_cfr1() @@ -887,7 +903,9 @@ def get(self, profile: TInt32 = DEFAULT_PROFILE def set_att_mu(self, att: TInt32): """Set digital step attenuator in machine units. - This method will write the attenuator settings of all four channels. See also + This method will write the attenuator settings of the channel + (Urukul proto_rev 0x08, all four channels will be updated at same time). + See also :meth:`CPLD.get_channel_att `. :param att: Attenuation setting, 8-bit digital. @@ -898,7 +916,9 @@ def set_att_mu(self, att: TInt32): def set_att(self, att: TFloat): """Set digital step attenuator in SI units. - This method will write the attenuator settings of all four channels. See also + This method will write the attenuator settings of the channel + (Urukul proto_rev 0x08, all four channels will be updated at same time). + See also :meth:`CPLD.get_channel_att `. :param att: Attenuation in dB. @@ -926,13 +946,53 @@ def get_att(self) -> TFloat: @kernel def cfg_sw(self, state: TBool): """Set CPLD CFG RF switch state. The RF switch is controlled by the - logical or of the CPLD configuration shift register + logical OR of the CPLD configuration shift register RF switch bit and the SW TTL line (if used). :param state: CPLD CFG RF switch bit """ self.cpld.cfg_sw(self.chip_select - 4, state) + @kernel + def cfg_osk(self, state: TBool): + """Set CPLD CFG OSK state. + + :param state: CPLD CFG OSK bit + """ + self.cpld.cfg_osk(self.chip_select - 4, state) + + @kernel + def cfg_drctl(self, state: TBool): + """Set CPLD CFG DRCTL state. + + :param state: CPLD CFG DRCTL bit + """ + self.cpld.cfg_drctl(self.chip_select - 4, state) + + @kernel + def cfg_drhold(self, state: TBool): + """Set CPLD CFG DRHOLD state. + + :param state: CPLD CFG DRHOLD bit + """ + self.cpld.cfg_drhold(self.chip_select - 4, state) + + @kernel + def cfg_mask_nu(self, state: TBool): + """Set CPLD CFG MASK_NU state. + + :param state: CPLD CFG MASK_NU bit + """ + self.cpld.cfg_mask_nu(self.chip_select - 4, state) + + @kernel + def cfg_att_en(self, state: TBool): + """Set CPLD CFG ATT_EN state. + + :param state: CPLD CFG ATT_EN bit + """ + self.cpld.cfg_att_en(self.chip_select - 4, state) + @kernel def set_sync(self, in_delay: TInt32, @@ -970,10 +1030,10 @@ def clear_smp_err(self): Also modifies CFR2. """ self.set_cfr2(sync_validation_disable=1) # clear SMP_ERR - self.cpld.io_update.pulse(1 * us) + self.io_update.pulse(1 * us) delay(10 * us) # slack self.set_cfr2(sync_validation_disable=0) # enable SMP_ERR - self.cpld.io_update.pulse(1 * us) + self.io_update.pulse(1 * us) @kernel def tune_sync_delay(self, @@ -1061,18 +1121,18 @@ def measure_io_update_alignment(self, delay_start: TInt64, t = now_mu() + 8 & ~7 at_mu(t + delay_start) # assumes a maximum t_SYNC_CLK period - self.cpld.io_update.pulse_mu(16 - delay_start) # realign + self.io_update.pulse_mu(16 - delay_start) # realign # disable DRG autoclear and LRR on io_update self.set_cfr1() # stop DRG self.write64(_AD9910_REG_RAMP_STEP, 0, 0) at_mu(t + 0x1000 + delay_stop) - self.cpld.io_update.pulse_mu(16 - delay_stop) # realign + self.io_update.pulse_mu(16 - delay_stop) # realign ftw = self.read32(_AD9910_REG_FTW) # read out effective FTW delay(100 * us) # slack # disable DRG self.set_cfr2(drg_enable=0) - self.cpld.io_update.pulse_mu(8) + self.io_update.pulse_mu(8) return ftw & 1 @kernel diff --git a/artiq/coredevice/urukul.py b/artiq/coredevice/urukul.py index cac53499b7..2d4beedbbe 100644 --- a/artiq/coredevice/urukul.py +++ b/artiq/coredevice/urukul.py @@ -1,10 +1,12 @@ -from numpy import int32, int64 +from abc import abstractmethod +from typing import Union -from artiq.language.core import kernel, delay, portable, at_mu, now_mu -from artiq.language.units import us, ms -from artiq.language.types import TInt32, TFloat, TBool +from numpy import int32, int64 from artiq.coredevice import spi2 as spi +from artiq.language.core import at_mu, delay, kernel, now_mu, portable +from artiq.language.types import TBool, TFloat, TInt32, TInt64 +from artiq.language.units import ms, us SPI_CONFIG = (0 * spi.SPI_OFFLINE | 0 * spi.SPI_END | 0 * spi.SPI_INPUT | 1 * spi.SPI_CS_POLARITY | @@ -20,30 +22,24 @@ SPIT_DDS_WR = 2 SPIT_DDS_RD = 16 -# CFG configuration register bit offsets +# Common CFG configuration register bit offsets CFG_RF_SW = 0 CFG_LED = 4 CFG_PROFILE = 8 -CFG_IO_UPDATE = 12 -CFG_MASK_NU = 13 -CFG_CLK_SEL0 = 17 -CFG_CLK_SEL1 = 21 -CFG_SYNC_SEL = 18 -CFG_RST = 19 -CFG_IO_RST = 20 -CFG_CLK_DIV = 22 - -# STA status register bit offsets + +# Common STA status register bit offsets STA_RF_SW = 0 STA_SMP_ERR = 4 STA_PLL_LOCK = 8 STA_IFC_MODE = 12 STA_PROTO_REV = 16 +STA_DROVER = 23 -# supported hardware and CPLD code version -STA_PROTO_REV_MATCH = 0x08 +# Supported hardware and CPLD code version +STA_PROTO_REV_8 = 0x08 +STA_PROTO_REV_9 = 0x09 -# chip select (decoded) +# Chip select (decoded) CS_CFG = 1 CS_ATT = 2 CS_DDS_MULTI = 3 @@ -56,62 +52,77 @@ DEFAULT_PROFILE = 7 -@portable -def urukul_cfg(rf_sw, led, profile, io_update, mask_nu, - clk_sel, sync_sel, rst, io_rst, clk_div): - """Build Urukul CPLD configuration register""" - return ((rf_sw << CFG_RF_SW) | - (led << CFG_LED) | - (profile << CFG_PROFILE) | - (io_update << CFG_IO_UPDATE) | - (mask_nu << CFG_MASK_NU) | - ((clk_sel & 0x01) << CFG_CLK_SEL0) | - ((clk_sel & 0x02) << (CFG_CLK_SEL1 - 1)) | - (sync_sel << CFG_SYNC_SEL) | - (rst << CFG_RST) | - (io_rst << CFG_IO_RST) | - (clk_div << CFG_CLK_DIV)) - - @portable def urukul_sta_rf_sw(sta): """Return the RF switch status from Urukul status register value.""" - return (sta >> STA_RF_SW) & 0xf + return (sta >> STA_RF_SW) & 0xF @portable def urukul_sta_smp_err(sta): """Return the SMP_ERR status from Urukul status register value.""" - return (sta >> STA_SMP_ERR) & 0xf + return (sta >> STA_SMP_ERR) & 0xF @portable def urukul_sta_pll_lock(sta): """Return the PLL_LOCK status from Urukul status register value.""" - return (sta >> STA_PLL_LOCK) & 0xf + return (sta >> STA_PLL_LOCK) & 0xF @portable def urukul_sta_ifc_mode(sta): """Return the IFC_MODE status from Urukul status register value.""" - return (sta >> STA_IFC_MODE) & 0xf + return (sta >> STA_IFC_MODE) & 0xF @portable def urukul_sta_proto_rev(sta): """Return the PROTO_REV value from Urukul status register value.""" - return (sta >> STA_PROTO_REV) & 0x7f + return (sta >> STA_PROTO_REV) & 0x7F + + +@portable +def urukul_sta_drover(sta): + """Return the DROVER status from Urukul status register value.""" + return (sta >> STA_DROVER) & 0xF class _RegIOUpdate: - def __init__(self, cpld): + def __init__(self, cpld, chip_select): self.cpld = cpld + self.chip_select = chip_select @kernel - def pulse(self, t: TFloat): + def pulse_mu(self, duration): + """Pulse the output high for the specified duration + (in machine units). + + The time cursor is advanced by the specified duration.""" cfg = self.cpld.cfg_reg - self.cpld.cfg_write(cfg | (1 << CFG_IO_UPDATE)) - delay(t) + if self.cpld.proto_rev == STA_PROTO_REV_8: + self.cpld.cfg_write(cfg | (1 << ProtoRev8.CFG_IO_UPDATE)) + else: + self.cpld.cfg_write( + cfg | (int64(1) << (ProtoRev9.CFG_IO_UPDATE + (self.chip_select - 4))) + ) + delay_mu(duration) + self.cpld.cfg_write(cfg) + + @kernel + def pulse(self, duration): + """Pulse the output high for the specified duration + (in seconds). + + The time cursor is advanced by the specified duration.""" + cfg = self.cpld.cfg_reg + if self.cpld.proto_rev == STA_PROTO_REV_8: + self.cpld.cfg_write(cfg | (1 << ProtoRev8.CFG_IO_UPDATE)) + else: + self.cpld.cfg_write( + cfg | (int64(1) << (ProtoRev9.CFG_IO_UPDATE + (self.chip_select - 4))) + ) + delay(duration) self.cpld.cfg_write(cfg) @@ -124,6 +135,492 @@ def set_mu(self, ftw: TInt32): pass +class CPLDVersion: + """ + Abstract base class for methods requiring version-specific CPLD implementations. + + Defines interface methods that must be customized for different CPLD versions. + """ + @abstractmethod + @kernel + def cfg_write(self, cpld, cfg): + pass + + @abstractmethod + @kernel + def sta_read(self, cpld): + pass + + @abstractmethod + @kernel + def init(self, cpld): + pass + + @abstractmethod + @kernel + def io_rst(self, cpld): + pass + + @abstractmethod + @kernel + def set_profile(self, cpld, channel, profile): + pass + + @kernel + def _configure_bit(self, cpld, bit_offset: TInt32, channel: TInt32, on: TBool): + pass + + @kernel + def _configure_all_bits(self, cpld, bit_offset: TInt32, state: TInt32): + pass + + @kernel + def cfg_mask_nu(self, cpld, channel: TInt32, on: TBool): + pass + + @kernel + def cfg_mask_nu_all(self, cpld, state: TInt32): + pass + + def _not_implemented(self, *args, **kwargs): + raise NotImplementedError( + "This function is not implemented for this Urukul version." + ) + + @kernel + def cfg_att_en(self, cpld, channel: TInt32, on: TBool): + self._not_implemented() + + @kernel + def cfg_att_en_all(self, cpld, state: TInt32): + self._not_implemented() + + @kernel + def cfg_osk(self, cpld, channel: TInt32, on: TBool): + self._not_implemented() + + @kernel + def cfg_osk_all(self, cpld, state: TInt32): + self._not_implemented() + + @kernel + def cfg_drctl(self, cpld, channel: TInt32, on: TBool): + self._not_implemented() + + @kernel + def cfg_drctl_all(self, cpld, state: TInt32): + self._not_implemented() + + @kernel + def cfg_drhold(self, cpld, channel: TInt32, on: TBool): + self._not_implemented() + + @kernel + def cfg_drhold_all(self, cpld, state: TInt32): + self._not_implemented() + + +class ProtoRev8(CPLDVersion): + """ + Implementation of the CPLD for Urkul ProtoRev8. + """ + + # ProtoRev8 CFG configuration register bit offsets + CFG_IO_UPDATE = 12 + CFG_MASK_NU = 13 + CFG_CLK_SEL0 = 17 + CFG_CLK_SEL1 = 21 + CFG_SYNC_SEL = 18 + CFG_RST = 19 + CFG_IO_RST = 20 + CFG_CLK_DIV = 22 + + @staticmethod + @portable + def urukul_cfg( + rf_sw, + led, + profile, + io_update, + mask_nu, + clk_sel, + sync_sel, + rst, + io_rst, + clk_div, + ): + """Build Urukul CPLD configuration register""" + return ( + (rf_sw << CFG_RF_SW) + | (led << CFG_LED) + | (profile << CFG_PROFILE) + | (io_update << ProtoRev8.CFG_IO_UPDATE) + | (mask_nu << ProtoRev8.CFG_MASK_NU) + | ((clk_sel & 0x01) << ProtoRev8.CFG_CLK_SEL0) + | ((clk_sel & 0x02) << (ProtoRev8.CFG_CLK_SEL1 - 1)) + | (sync_sel << ProtoRev8.CFG_SYNC_SEL) + | (rst << ProtoRev8.CFG_RST) + | (io_rst << ProtoRev8.CFG_IO_RST) + | (clk_div << ProtoRev8.CFG_CLK_DIV) + ) + + @kernel + def cfg_write(self, cpld, cfg: TInt32): + """Write to the configuration register. + + See :func:`urukul_cfg` for possible flags. + + :param cfg: 24-bit data to be written. Will be stored at + :attr:`cfg_reg`. + """ + cpld.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, 24, SPIT_CFG_WR, CS_CFG) + cpld.bus.write(cfg << 8) + cpld.cfg_reg = cfg + + @kernel + def sta_read(self, cpld) -> TInt32: + """Read the status register. + + Use any of the following functions to extract values: + + * :func:`urukul_sta_rf_sw` + * :func:`urukul_sta_smp_err` + * :func:`urukul_sta_pll_lock` + * :func:`urukul_sta_ifc_mode` + * :func:`urukul_sta_proto_rev` + + :return: The status register value. + """ + cpld.bus.set_config_mu( + SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, 24, SPIT_CFG_RD, CS_CFG + ) + cpld.bus.write(cpld.cfg_reg << 8) + return cpld.bus.read() + + @kernel + def init(self, cpld): + """Initialize Urukul with ProtoRev8. + + Resets the DDS I/O interface. + Does not pulse the DDS ``MASTER_RESET`` as that confuses the AD9910. + """ + if urukul_sta_proto_rev(self.sta_read(cpld)) != STA_PROTO_REV_8: + raise ValueError("Urukul proto_rev mismatch") + cfg = cpld.cfg_reg + # Don't pulse MASTER_RESET (m-labs/artiq#940) + cpld.cfg_reg = cfg | (0 << ProtoRev8.CFG_RST) | (1 << ProtoRev8.CFG_IO_RST) + delay(100 * us) # reset, slack + cpld.cfg_write(cfg) + if cpld.sync_div: + at_mu(now_mu() & ~0xF) # align to RTIO/2 + cpld.set_sync_div(cpld.sync_div) # 125 MHz/2 = 1 GHz/16 + delay(1 * ms) # DDS wake up + + @kernel + def io_rst(self, cpld): + """Pulse IO_RST""" + cpld.cfg_write(cpld.cfg_reg | (1 << ProtoRev8.CFG_IO_RST)) + cpld.cfg_write(cpld.cfg_reg & ~(1 << ProtoRev8.CFG_IO_RST)) + + @kernel + def set_profile(self, cpld, channel: TInt32, profile: TInt32): + """Set the PROFILE pins. + + The PROFILE pins are common to all four DDS channels. + + :param channel: Channel index (0-3). Unused (here for backwards compatability). + :param profile: PROFILE pins in numeric representation (0-7). + """ + cfg = cpld.cfg_reg & ~(7 << CFG_PROFILE) + cfg |= (profile & 7) << CFG_PROFILE + cpld.cfg_write(cfg) + + @kernel + def _configure_bit(self, cpld, bit_offset: TInt32, channel: TInt32, on: TBool): + """Configure a single bit in the configuration register. + + :param bit_offset: Base bit offset for the configuration type + :param channel: Channel index (0-3) + :param on: Switch value + """ + c = cpld.cfg_reg + if on: + c |= 1 << (bit_offset + channel) + else: + c &= ~(1 << (bit_offset + channel)) + cpld.cfg_write(c) + + @kernel + def _configure_all_bits(self, cpld, bit_offset: TInt32, state: TInt32): + """Configure all four bits at a specific bit offset in the configuration register. + + :param bit_offset: bit offset for the configuration bits + :param state: State as a 4-bit integer + """ + cpld.cfg_write( + (cpld.cfg_reg & ~(0xF << bit_offset)) | (int64(state) << bit_offset) + ) + + @kernel + def cfg_mask_nu(self, cpld, channel: TInt32, on: TBool): + """Configure the MASK_NU bit for the given channel in the configuration register. + + :param channel: Channel index (0-3) + :param on: Switch value + """ + cpld._configure_bit(ProtoRev8.CFG_MASK_NU, channel, on) + + @kernel + def cfg_mask_nu_all(self, cpld, state: TInt32): + """Configure all four MASK_NU bits in the configuration register. + + :param state: MASK_NU state as a 4-bit integer. + """ + cpld._configure_all_bits(ProtoRev8.CFG_MASK_NU, state) + + +class ProtoRev9(CPLDVersion): + """ + Implementation of the CPLD for Urkul ProtoRev9. + """ + + # ProtoRev9 CFG configuration register bit offsets + CFG_OSK = 20 + CFG_DRCTL = 24 + CFG_DRHOLD = 28 + CFG_IO_UPDATE = 32 + CFG_MASK_NU = 36 + CFG_CLK_SEL0 = 40 + CFG_CLK_SEL1 = 44 + CFG_SYNC_SEL = 41 + CFG_RST = 42 + CFG_IO_RST = 43 + CFG_CLK_DIV = 45 + CFG_ATT_EN = 47 + + @staticmethod + @portable + def urukul_cfg( + rf_sw, + led, + profile, + osk, + drctl, + drhold, + io_update, + mask_nu, + clk_sel, + sync_sel, + rst, + io_rst, + clk_div, + att_en, + ): + """Build Urukul CPLD configuration register""" + return ( + (rf_sw << CFG_RF_SW) + | (led << CFG_LED) + | (profile << CFG_PROFILE) + | (osk << ProtoRev9.CFG_OSK) + | (drctl << ProtoRev9.CFG_DRCTL) + | (drhold << ProtoRev9.CFG_DRHOLD) + | (io_update << ProtoRev9.CFG_IO_UPDATE) + | (mask_nu << ProtoRev9.CFG_MASK_NU) + | ((clk_sel & 0x01) << ProtoRev9.CFG_CLK_SEL0) + | ((clk_sel & 0x02) << (ProtoRev9.CFG_CLK_SEL1 - 1)) + | (sync_sel << ProtoRev9.CFG_SYNC_SEL) + | (rst << ProtoRev9.CFG_RST) + | (io_rst << ProtoRev9.CFG_IO_RST) + | (clk_div << ProtoRev9.CFG_CLK_DIV) + | (att_en << ProtoRev9.CFG_ATT_EN) + ) + + @kernel + def cfg_write(self, cpld, cfg: TInt64): + """Write to the configuration register. + + See :func:`urukul_cfg` for possible flags. + + :param cfg: 52-bit data to be written. Will be stored at + :attr:`cfg_reg`. + """ + cpld.bus.set_config_mu(SPI_CONFIG, 24, SPIT_CFG_WR, CS_CFG) + cpld.bus.write(((cfg >> 28) & 0xFFFFFF) << 8) + cpld.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, 28, SPIT_CFG_WR, CS_CFG) + cpld.bus.write((cfg & 0xFFFFFFF) << 4) + cpld.cfg_reg = cfg + + @kernel + def sta_read(self, cpld) -> TInt32: + """Read the status register. + + Use any of the following functions to extract values: + + * :func:`urukul_sta_rf_sw` + * :func:`urukul_sta_smp_err` + * :func:`urukul_sta_pll_lock` + * :func:`urukul_sta_ifc_mode` + * :func:`urukul_sta_proto_rev` + * :func:`urukul_sta_drover` + + :return: The status register value. + """ + cpld.bus.set_config_mu(SPI_CONFIG, 24, SPIT_CFG_WR, CS_CFG) + cpld.bus.write(((cpld.cfg_reg >> 24) & 0xFFFFFF) << 8) + cpld.bus.set_config_mu( + SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, 28, SPIT_CFG_RD, CS_CFG + ) + cpld.bus.write((cpld.cfg_reg & 0xFFFFFFF) << 4) + return cpld.bus.read() + + @kernel + def init(self, cpld): + """Initialize Urukul with ProtoRev9. + + Resets the DDS I/O interface. + Does not pulse the DDS ``MASTER_RESET`` as that confuses the AD9910. + """ + if urukul_sta_proto_rev(self.sta_read(cpld)) != STA_PROTO_REV_9: + raise ValueError("Urukul proto_rev mismatch") + cfg = cpld.cfg_reg + # Don't pulse MASTER_RESET (m-labs/artiq#940) + cpld.cfg_reg = cfg | (0 << ProtoRev9.CFG_RST) | (1 << ProtoRev9.CFG_IO_RST) + delay(100 * us) # reset, slack + cpld.cfg_write(cfg) + if cpld.sync_div: + at_mu(now_mu() & ~0xF) # align to RTIO/2 + cpld.set_sync_div(cpld.sync_div) # 125 MHz/2 = 1 GHz/16 + delay(1 * ms) # DDS wake up + + @kernel + def io_rst(self, cpld): + """Pulse IO_RST""" + cpld.cfg_write(cpld.cfg_reg | (1 << ProtoRev9.CFG_IO_RST)) + cpld.cfg_write(cpld.cfg_reg & ~(1 << ProtoRev9.CFG_IO_RST)) + + @kernel + def set_profile(self, cpld, channel: TInt32, profile: TInt32): + """Set the CFG.PROFILE[0:2] pins for the given channel. + + :param profile: PROFILE pins in numeric representation (0-7). + :param channel: Channel (0-3). + """ + cfg = cpld.cfg_reg & ~(7 << (CFG_PROFILE + channel * 3)) + cfg |= (profile & 7) << (CFG_PROFILE + channel * 3) + cpld.cfg_write(cfg) + + @kernel + def _configure_bit(self, cpld, bit_offset: TInt32, channel: TInt32, on: TBool): + """Configure a single bit in the configuration register. + + :param bit_offset: Base bit offset for the configuration type + :param channel: Channel index (0-3) + :param on: Switch value + """ + c = cpld.cfg_reg + if on: + c |= int64(1) << (bit_offset + channel) + else: + c &= ~(int64(1) << (bit_offset + channel)) + cpld.cfg_write(c) + + @kernel + def _configure_all_bits(self, cpld, bit_offset: TInt32, state: TInt32): + """Configure all four bits at a specific bit offset in the configuration register. + + :param bit_offset: bit offset for the configuration bits + :param state: State as a 4-bit integer + """ + cpld.cfg_write( + (cpld.cfg_reg & ~(int64(0xF) << bit_offset)) | (int64(state) << bit_offset) + ) + + @kernel + def cfg_mask_nu(self, cpld, channel: TInt32, on: TBool): + """Configure the MASK_NU bit for the given channel in the configuration register. + + :param channel: Channel index (0-3) + :param on: Switch value + """ + cpld._configure_bit(ProtoRev9.CFG_MASK_NU, channel, on) + + @kernel + def cfg_mask_nu_all(self, cpld, state: TInt32): + """Configure all four MASK_NU bits in the configuration register. + + :param state: MASK_NU state as a 4-bit integer. + """ + cpld._configure_all_bits(ProtoRev9.CFG_MASK_NU, state) + + @kernel + def cfg_att_en(self, cpld, channel: TInt32, on: TBool): + """Configure the ATT_EN bit for the given channel in the configuration register. + + :param channel: Channel index (0-3) + :param on: Switch value + """ + cpld._configure_bit(ProtoRev9.CFG_ATT_EN, channel, on) + + @kernel + def cfg_att_en_all(self, cpld, state: TInt32): + """Configure all four ATT_EN bits in the configuration register. + + :param state: OSK state as a 4-bit integer. + """ + cpld._configure_all_bits(ProtoRev9.CFG_ATT_EN, state) + + @kernel + def cfg_osk(self, cpld, channel: TInt32, on: TBool): + """Configure the OSK bit for the given channel in the configuration register. + + :param channel: Channel index (0-3) + :param on: Switch value + """ + cpld._configure_bit(ProtoRev9.CFG_OSK, channel, on) + + @kernel + def cfg_osk_all(self, cpld, state: TInt32): + """Configure all four OSK bits in the configuration register. + + :param state: OSK state as a 4-bit integer. + """ + cpld._configure_all_bits(ProtoRev9.CFG_OSK, state) + + @kernel + def cfg_drctl(self, cpld, channel: TInt32, on: TBool): + """Configure the DRCTL bit for the given channel in the configuration register. + + :param channel: Channel index (0-3) + :param on: Switch value + """ + cpld._configure_bit(ProtoRev9.CFG_DRCTL, channel, on) + + @kernel + def cfg_drctl_all(self, cpld, state: TInt32): + """Configure all four DRCTL bits in the configuration register. + + :param state: DRCTL state as a 4-bit integer. + """ + cpld._configure_all_bits(ProtoRev9.CFG_DRCTL, state) + + @kernel + def cfg_drhold(self, cpld, channel: TInt32, on: TBool): + """Configure the DRHOLD bit for the given channel in the configuration register. + + :param channel: Channel index (0-3) + :param on: Switch value + """ + cpld._configure_bit(ProtoRev9.CFG_DRHOLD, channel, on) + + @kernel + def cfg_drhold_all(self, cpld, state: TInt32): + """Configure all four DRHOLD bits in the configuration register. + + :param state: DRHOLD state as a 4-bit integer. + """ + cpld._configure_all_bits(ProtoRev9.CFG_DRHOLD, state) + + class CPLD: """Urukul CPLD SPI router and configuration interface. @@ -156,19 +653,21 @@ class CPLD: :param sync_div: ``SYNC_IN`` generator divider. The ratio between the coarse RTIO frequency and the ``SYNC_IN`` generator frequency (default: 2 if `sync_device` was specified). + :param proto_rev: CPLD protocol revision :param core_device: Core device name If the clocking is incorrect (for example, setting ``clk_sel`` to the front panel SMA with no clock connected), then the ``init()`` method of the DDS channels can fail with the error message ``PLL lock timeout``. """ + kernel_invariants = {"refclk", "bus", "core", "io_update", "clk_div"} def __init__(self, dmgr, spi_device, io_update_device=None, dds_reset_device=None, sync_device=None, sync_sel=0, clk_sel=0, clk_div=0, rf_sw=0, refclk=125e6, att=0x00000000, sync_div=None, - core_device="core"): + proto_rev=0x08, core_device="core"): self.core = dmgr.get(core_device) self.refclk = refclk @@ -179,7 +678,7 @@ def __init__(self, dmgr, spi_device, io_update_device=None, if io_update_device is not None: self.io_update = dmgr.get(io_update_device) else: - self.io_update = _RegIOUpdate(self) + self.io_update = io_update_device if dds_reset_device is not None: self.dds_reset = dmgr.get(dds_reset_device) if sync_device is not None: @@ -191,77 +690,117 @@ def __init__(self, dmgr, spi_device, io_update_device=None, assert sync_div is None sync_div = 0 - self.cfg_reg = urukul_cfg(rf_sw=rf_sw, led=0, profile=DEFAULT_PROFILE, - io_update=0, mask_nu=0, clk_sel=clk_sel, - sync_sel=sync_sel, - rst=0, io_rst=0, clk_div=clk_div) + self.proto_rev = proto_rev + if proto_rev == STA_PROTO_REV_8: + self.version = ProtoRev8() + elif proto_rev == STA_PROTO_REV_9: + self.version = ProtoRev9() + else: + raise ValueError(f"Urukul unsupported proto_rev: {proto_rev}") + + if self.proto_rev == STA_PROTO_REV_8: + self.cfg_reg = ProtoRev8.urukul_cfg( + rf_sw=rf_sw, + led=0, + profile=DEFAULT_PROFILE, + io_update=0, + mask_nu=0, + clk_sel=clk_sel, + sync_sel=sync_sel, + rst=0, + io_rst=0, + clk_div=clk_div, + ) + else: + self.cfg_reg = ProtoRev9.urukul_cfg( + rf_sw=rf_sw, + led=0, + profile=(DEFAULT_PROFILE << 9) + | (DEFAULT_PROFILE << 6) + | (DEFAULT_PROFILE << 3) + | DEFAULT_PROFILE, + osk=0, + drctl=0, + drhold=0, + io_update=0, + mask_nu=0, + clk_sel=clk_sel, + sync_sel=sync_sel, + rst=0, + io_rst=0, + clk_div=clk_div, + att_en=0, + ) self.att_reg = int32(int64(att)) self.sync_div = sync_div @kernel - def cfg_write(self, cfg: TInt32): - """Write to the configuration register. + def cfg_write(self, cfg): + self.version.cfg_write(self, cfg) - See :func:`urukul_cfg` for possible flags. + @kernel + def sta_read(self): + return self.version.sta_read(self) - :param cfg: 24-bit data to be written. Will be stored at - :attr:`cfg_reg`. - """ - self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, 24, - SPIT_CFG_WR, CS_CFG) - self.bus.write(cfg << 8) - self.cfg_reg = cfg + @kernel + def init(self): + self.version.init(self) @kernel - def sta_read(self) -> TInt32: - """Read the status register. + def io_rst(self): + self.version.io_rst(self) - Use any of the following functions to extract values: + @kernel + def set_profile(self, channel, profile): + self.version.set_profile(self, channel, profile) - * :func:`urukul_sta_rf_sw` - * :func:`urukul_sta_smp_err` - * :func:`urukul_sta_pll_lock` - * :func:`urukul_sta_ifc_mode` - * :func:`urukul_sta_proto_rev` + @kernel + def _configure_bit(self, bit_offset: TInt32, channel: TInt32, on: TBool): + self.version._configure_bit(self, bit_offset, channel, on) - :return: The status register value. - """ - self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, 24, - SPIT_CFG_RD, CS_CFG) - self.bus.write(self.cfg_reg << 8) - return self.bus.read() + @kernel + def _configure_all_bits(self, bit_offset: TInt32, state: TInt32): + self.version._configure_all_bits(self, bit_offset, state) @kernel - def init(self, blind: TBool = False): - """Initialize and detect Urukul. + def cfg_mask_nu(self, channel: TInt32, on: TBool): + self.version.cfg_mask_nu(self, channel, on) - Resets the DDS I/O interface and verifies correct CPLD gateware - version. - Does not pulse the DDS ``MASTER_RESET`` as that confuses the AD9910. + @kernel + def cfg_mask_nu_all(self, state: TInt32): + self.version.cfg_mask_nu_all(self, state) - :param blind: Do not attempt to verify presence and compatibility. - """ - cfg = self.cfg_reg - # Don't pulse MASTER_RESET (m-labs/artiq#940) - self.cfg_reg = cfg | (0 << CFG_RST) | (1 << CFG_IO_RST) - if blind: - self.cfg_write(self.cfg_reg) - else: - proto_rev = urukul_sta_proto_rev(self.sta_read()) - if proto_rev != STA_PROTO_REV_MATCH: - raise ValueError("Urukul proto_rev mismatch") - delay(100 * us) # reset, slack - self.cfg_write(cfg) - if self.sync_div: - at_mu(now_mu() & ~0xf) # align to RTIO/2 - self.set_sync_div(self.sync_div) # 125 MHz/2 = 1 GHz/16 - delay(1 * ms) # DDS wake up + @kernel + def cfg_att_en(self, channel: TInt32, on: TBool): + self.version.cfg_att_en(self, channel, on) @kernel - def io_rst(self): - """Pulse IO_RST""" - self.cfg_write(self.cfg_reg | (1 << CFG_IO_RST)) - self.cfg_write(self.cfg_reg & ~(1 << CFG_IO_RST)) + def cfg_att_en_all(self, state: TInt32): + self.version.cfg_att_en_all(self, state) + + @kernel + def cfg_osk(self, channel: TInt32, on: TBool): + self.version.cfg_osk(self, channel, on) + + @kernel + def cfg_osk_all(self, state: TInt32): + self.version.cfg_osk_all(self, state) + + @kernel + def cfg_drctl(self, channel: TInt32, on: TBool): + self.version.cfg_drctl(self, channel, on) + + @kernel + def cfg_drctl_all(self, state: TInt32): + self.version.cfg_drctl_all(self, state) + + @kernel + def cfg_drhold(self, channel: TInt32, on: TBool): + self.version.cfg_drhold(self, channel, on) + + @kernel + def cfg_drhold_all(self, state: TInt32): + self.version.cfg_drhold_all(self, state) @kernel def cfg_sw(self, channel: TInt32, on: TBool): @@ -285,7 +824,7 @@ def cfg_switches(self, state: TInt32): :param state: RF switch state as a 4-bit integer. """ - self.cfg_write((self.cfg_reg & ~0xf) | state) + self.cfg_write((self.cfg_reg & ~0xF) | state) @portable(flags={"fast-math"}) def mu_to_att(self, att_mu: TInt32) -> TFloat: @@ -294,7 +833,7 @@ def mu_to_att(self, att_mu: TInt32) -> TFloat: :param att_mu: Digital attenuation setting. :return: Attenuation setting in dB. """ - return (255 - (att_mu & 0xff)) / 8 + return (255 - (att_mu & 0xFF)) / 8 @portable(flags={"fast-math"}) def att_to_mu(self, att: TFloat) -> TInt32: @@ -320,13 +859,13 @@ def set_att_mu(self, channel: TInt32, att: TInt32): :param att: 8-bit digital attenuation setting: 255 minimum attenuation, 0 maximum attenuation (31.5 dB) """ - a = self.att_reg & ~(0xff << (channel * 8)) + a = self.att_reg & ~(0xFF << (channel * 8)) a |= att << (channel * 8) self.set_all_att_mu(a) @kernel - def set_all_att_mu(self, att_reg: TInt32): - """Set all four digital step attenuators (in machine units). + def set_all_att_mu(self, att_reg: TInt32): + """Set all four digital step attenuators (in machine units). See also :meth:`set_att_mu`. :param att_reg: Attenuator setting string (32-bit) @@ -340,7 +879,6 @@ def set_all_att_mu(self, att_reg: TInt32): def set_att(self, channel: TInt32, att: TFloat): """Set digital step attenuator in SI units. - This method will write the attenuator settings of all four channels. See also :meth:`set_att_mu`. :param channel: Attenuator channel (0-3). @@ -384,7 +922,7 @@ def get_channel_att_mu(self, channel: TInt32) -> TInt32: :return: 8-bit digital attenuation setting: 255 minimum attenuation, 0 maximum attenuation (31.5 dB) """ - return int32((self.get_att_mu() >> (channel * 8)) & 0xff) + return int32((self.get_att_mu() >> (channel * 8)) & 0xFF) @kernel def get_channel_att(self, channel: TInt32) -> TFloat: @@ -415,15 +953,3 @@ def set_sync_div(self, div: TInt32): ftw = ftw_max // div assert ftw * div == ftw_max self.sync.set_mu(ftw) - - @kernel - def set_profile(self, profile: TInt32): - """Set the PROFILE pins. - - The PROFILE pins are common to all four DDS channels. - - :param profile: PROFILE pins in numeric representation (0-7). - """ - cfg = self.cfg_reg & ~(7 << CFG_PROFILE) - cfg |= (profile & 7) << CFG_PROFILE - self.cfg_write(cfg) diff --git a/artiq/frontend/artiq_ddb_template.py b/artiq/frontend/artiq_ddb_template.py index f19f9cc183..c18d437c72 100755 --- a/artiq/frontend/artiq_ddb_template.py +++ b/artiq/frontend/artiq_ddb_template.py @@ -215,6 +215,7 @@ def process_urukul(self, rtio_offset, peripheral): clk_div = peripheral.get("clk_div") if clk_div is None: clk_div = 0 if pll_en else 1 + proto_rev = peripheral["proto_rev"] self.gen(""" device_db["eeprom_{name}"] = {{ @@ -275,14 +276,16 @@ def process_urukul(self, rtio_offset, peripheral): "io_update_device": "ttl_{name}_io_update", "refclk": {refclk}, "clk_sel": {clk_sel}, - "clk_div": {clk_div} + "clk_div": {clk_div}, + "proto_rev": {proto_rev} }} }}""", name=urukul_name, sync_device="\"ttl_{name}_sync\"".format(name=urukul_name) if synchronization else "None", refclk=peripheral.get("refclk", self.primary_description["rtio_frequency"]), clk_sel=peripheral["clk_sel"], - clk_div=clk_div) + clk_div=clk_div, + proto_rev=proto_rev) dds = peripheral["dds"] pll_vco = peripheral.get("pll_vco") for i in range(4): @@ -542,7 +545,8 @@ def process_suservo(self, rtio_offset, peripheral): "arguments": {{ "spi_device": "spi_{urukul_name}", "refclk": {refclk}, - "clk_sel": {clk_sel} + "clk_sel": {clk_sel}, + "proto_rev": {proto_rev} }} }} device_db["{urukul_name}_dds"] = {{ @@ -560,6 +564,7 @@ def process_suservo(self, rtio_offset, peripheral): urukul_channel=rtio_offset+next(channel), refclk=peripheral.get("refclk", self.primary_description["rtio_frequency"]), clk_sel=peripheral["clk_sel"], + proto_rev=peripheral["proto_rev"], pll_vco=",\n \"pll_vco\": {}".format(pll_vco) if pll_vco is not None else "", pll_n=peripheral["pll_n"], pll_en=peripheral["pll_en"]) return next(channel) diff --git a/artiq/gateware/eem.py b/artiq/gateware/eem.py index 71f5bb0feb..a9298c6b3a 100644 --- a/artiq/gateware/eem.py +++ b/artiq/gateware/eem.py @@ -226,7 +226,8 @@ def io_qspi(eem0, eem1, iostandard): return ios @classmethod - def add_std(cls, target, eem, eem_aux, ttl_out_cls, dds_type, sync_gen_cls=None, iostandard=default_iostandard): + def add_std(cls, target, eem, eem_aux, ttl_out_cls, dds_type, proto_rev, + sync_gen_cls=None, iostandard=default_iostandard): cls.add_extension(target, eem, eem_aux, iostandard=iostandard) spi_phy = spi2.SPIMaster(target.platform.request("urukul{}_spi_p".format(eem)), @@ -245,7 +246,7 @@ def add_std(cls, target, eem, eem_aux, ttl_out_cls, dds_type, sync_gen_cls=None, target.submodules += io_upd_phy target.rtio_channels.append(rtio.Channel.from_phy(io_upd_phy)) - dds_monitor = dds.UrukulMonitor(spi_phy, io_upd_phy, dds_type) + dds_monitor = dds.UrukulMonitor(spi_phy, io_upd_phy, dds_type, proto_rev) target.submodules += dds_monitor spi_phy.probes.extend(dds_monitor.probes) diff --git a/artiq/gateware/eem_7series.py b/artiq/gateware/eem_7series.py index 2ca444d37f..7053240385 100644 --- a/artiq/gateware/eem_7series.py +++ b/artiq/gateware/eem_7series.py @@ -47,7 +47,7 @@ def peripheral_urukul(module, peripheral, **kwargs): else: sync_gen_cls = None eem.Urukul.add_std(module, port, port_aux, ttl_serdes_7series.Output_8X, - peripheral["dds"], sync_gen_cls, **kwargs) + peripheral["dds"], peripheral["proto_rev"], sync_gen_cls, **kwargs) def peripheral_novogorny(module, peripheral, **kwargs): diff --git a/artiq/gateware/rtio/phy/dds.py b/artiq/gateware/rtio/phy/dds.py index ca7494c20d..1529f005c8 100644 --- a/artiq/gateware/rtio/phy/dds.py +++ b/artiq/gateware/rtio/phy/dds.py @@ -4,7 +4,15 @@ from artiq.gateware.rtio.phy.wishbone import RT2WB from artiq.coredevice.spi2 import SPI_CONFIG_ADDR, SPI_DATA_ADDR, SPI_END -from artiq.coredevice.urukul import CS_DDS_CH0, CS_DDS_MULTI, CFG_IO_UPDATE, CS_CFG +from artiq.coredevice.urukul import ( + CS_CFG, + CS_DDS_CH0, + CS_DDS_MULTI, + STA_PROTO_REV_8, + STA_PROTO_REV_9, + ProtoRev8, + ProtoRev9, +) from artiq.coredevice.ad9912_reg import AD9912_POW1 from artiq.coredevice.ad9910 import _AD9910_REG_PROFILE0, _AD9910_REG_PROFILE7, _AD9910_REG_FTW @@ -62,7 +70,7 @@ def selected(c): class UrukulMonitor(Module): - def __init__(self, spi_phy, io_update_phy, dds, nchannels=4): + def __init__(self, spi_phy, io_update_phy, dds, proto_rev, nchannels=4): self.spi_phy = spi_phy self.io_update_phy = io_update_phy @@ -102,12 +110,24 @@ def __init__(self, spi_phy, io_update_phy, dds, nchannels=4): self.submodules += monitor self.sync.rio_phy += [ - If(ch_sel & self.is_io_update(), self.probes[i].eq(monitor.ftw)) + If(ch_sel & self.is_io_update(proto_rev, flags), self.probes[i].eq(monitor.ftw)) ] - def is_io_update(self): - # shifted 8 bits left for 32-bit bus - reg_io_upd = (self.cs == CS_CFG) & self.current_data[8 + CFG_IO_UPDATE] + def is_io_update(self, proto_rev, flags): + if proto_rev == STA_PROTO_REV_8: + # shifted 8 bits left for 32-bit bus + reg_io_upd = (self.cs == CS_CFG) & self.current_data[8 + ProtoRev8.CFG_IO_UPDATE] + elif proto_rev == STA_PROTO_REV_9: + # cfg_write for proto_rev 9 takes two transfers, CFG_IO_UPDATES are in the last one + reg_io_upd = (self.cs == CS_CFG) & (flags & SPI_END) & ( + self.current_data[4 + (ProtoRev9.CFG_IO_UPDATE - 32)] | + self.current_data[4 + ((ProtoRev9.CFG_IO_UPDATE + 1) - 32)] | + self.current_data[4 + ((ProtoRev9.CFG_IO_UPDATE + 2) - 32)] | + self.current_data[4 + ((ProtoRev9.CFG_IO_UPDATE + 3) - 32)] + ) + else: + raise ValueError("Invalid proto_rev") + phy_io_upd = False if self.io_update_phy: phy_io_upd = self.io_update_phy.rtlink.o.stb & self.io_update_phy.rtlink.o.data diff --git a/artiq/test/coredevice/test_ad9910.py b/artiq/test/coredevice/test_ad9910.py index e2232f67bd..b66e13553e 100644 --- a/artiq/test/coredevice/test_ad9910.py +++ b/artiq/test/coredevice/test_ad9910.py @@ -1,17 +1,30 @@ +from numpy import int64 + from artiq.experiment import * from artiq.test.hardware_testbench import ExperimentCase from artiq.coredevice.ad9910 import ( - _AD9910_REG_FTW, _AD9910_REG_PROFILE0, RAM_MODE_RAMPUP, - RAM_DEST_FTW) + _AD9910_REG_FTW, + _AD9910_REG_PROFILE0, + RAM_MODE_RAMPUP, + RAM_DEST_FTW, +) from artiq.coredevice.urukul import ( - urukul_sta_smp_err, CFG_CLK_SEL0, CFG_CLK_SEL1) + STA_PROTO_REV_8, + ProtoRev8, + ProtoRev9, + urukul_sta_smp_err, +) +from artiq.test.coredevice.test_ad9910_waveform import io_update_device + +DDS = "urukul_ad9910" class AD9910Exp(EnvExperiment): - def build(self, runner): + def build(self, runner, io_update_device=True): self.setattr_device("core") - self.dev = self.get_device("urukul_ad9910") + self.dev = self.get_device(DDS) self.runner = runner + self.io_update_device = io_update_device def run(self): getattr(self, self.runner)() @@ -24,23 +37,43 @@ def instantiate(self): def init(self): self.core.break_realtime() self.dev.cpld.init() + if not self.io_update_device: + # Set MASK_NU to trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(True) self.dev.init() + if not self.io_update_device: + # Unset MASK_NU to un-trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(False) @kernel - def init_fail(self): + def init_fail_proto_rev(self): self.core.break_realtime() self.dev.cpld.init() cfg = self.dev.cpld.cfg_reg - cfg &= ~(1 << CFG_CLK_SEL1) - cfg |= 1 << CFG_CLK_SEL0 + if self.dev.cpld.proto_rev == STA_PROTO_REV_8: + cfg &= ~(1 << ProtoRev8.CFG_CLK_SEL1) + cfg |= 1 << ProtoRev8.CFG_CLK_SEL0 + else: + cfg &= ~(1 << ProtoRev9.CFG_CLK_SEL1) + cfg |= 1 << ProtoRev9.CFG_CLK_SEL0 + self.dev.cpld.cfg_write(cfg) # clk_sel=1, external SMA, should fail PLL lock + if not self.io_update_device: + # Set MASK_NU to trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(True) self.dev.init() + if not self.io_update_device: + # Unset MASK_NU to un-trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(False) @kernel def set_get(self): self.core.break_realtime() self.dev.cpld.init() + if not self.io_update_device: + # Set MASK_NU to trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(True) self.dev.init() f = 81.2345*MHz p = .33 @@ -63,10 +96,18 @@ def set_get(self): self.set_dataset("att_set", self.dev.cpld.att_to_mu(att)) self.set_dataset("att_get", att_mu) + if not self.io_update_device: + self.core.break_realtime() + # Unset MASK_NU to un-trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(False) + @kernel def set_get_io_update_regs(self): self.core.break_realtime() self.dev.cpld.init() + if not self.io_update_device: + # Set MASK_NU to trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(True) self.dev.init() f = 81.2345*MHz p = .33 @@ -89,18 +130,30 @@ def set_get_io_update_regs(self): self.set_dataset("asf_set", self.dev.amplitude_to_asf(a)) self.set_dataset("asf_get", asf) + if not self.io_update_device: + self.core.break_realtime() + # Unset MASK_NU to un-trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(False) + @kernel def read_write64(self): self.core.break_realtime() self.dev.cpld.init() + if not self.io_update_device: + # Set MASK_NU to trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(True) self.dev.init() lo = 0x12345678 hi = 0x09abcdef self.dev.write64(_AD9910_REG_PROFILE0, hi, lo) - self.dev.cpld.io_update.pulse_mu(8) + self.dev.io_update.pulse_mu(8) read = self.dev.read64(_AD9910_REG_PROFILE0) self.set_dataset("write", (int64(hi) << 32) | lo) self.set_dataset("read", read) + if not self.io_update_device: + self.core.break_realtime() + # Unset MASK_NU to un-trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(False) @kernel def set_speed(self): @@ -131,6 +184,9 @@ def set_speed_mu(self): def sync_window(self): self.core.break_realtime() self.dev.cpld.init() + if not self.io_update_device: + # Set MASK_NU to trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(True) self.dev.init() err = [0] * 32 for i in range(6): @@ -144,6 +200,9 @@ def sync_window(self): self.set_dataset("dly", dly) self.set_dataset("win", win) self.set_dataset("err", err) + if not self.io_update_device: + # Unset MASK_NU to un-trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(False) @kernel def sync_scan(self, err, win): @@ -173,14 +232,17 @@ def scan_io_delay(self, bins1, bins2): n = 100 for i in range(n): for j in range(len(bins1)): - bins1[j] += self.dev.measure_io_update_alignment(j, j + 1) - bins2[j] += self.dev.measure_io_update_alignment(j, j + 2) + bins1[j] += self.dev.measure_io_update_alignment(int64(j), j + 1) + bins2[j] += self.dev.measure_io_update_alignment(int64(j), j + 2) delay(10*ms) @kernel def sw_readback(self): self.core.break_realtime() self.dev.cpld.init() + if not self.io_update_device: + # Set MASK_NU to trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(True) self.dev.init() self.dev.cfg_sw(False) self.dev.sw.on() @@ -189,24 +251,33 @@ def sw_readback(self): self.dev.sw.off() sw_off = (self.dev.cpld.sta_read() >> (self.dev.chip_select - 4)) & 1 self.set_dataset("sw", (sw_on, sw_off)) + if not self.io_update_device: + # Unset MASK_NU to un-trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(False) @kernel def profile_readback(self): self.core.break_realtime() self.dev.cpld.init() + if not self.io_update_device: + # Set MASK_NU to trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(True) self.dev.init() for i in range(8): self.dev.set_mu(ftw=i, profile=i) ftw = [0] * 8 for i in range(8): - self.dev.cpld.set_profile(i) + self.dev.cpld.set_profile(0, i) # If PROFILE is not alligned to SYNC_CLK a multi-bit change # doesn't transfer cleanly. Use IO_UPDATE to load the profile # again. - self.dev.cpld.io_update.pulse_mu(8) + self.dev.io_update.pulse_mu(8) ftw[i] = self.dev.read32(_AD9910_REG_FTW) delay(100*us) self.set_dataset("ftw", ftw) + if not self.io_update_device: + # Unset MASK_NU to un-trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(False) @kernel def ram_write(self): @@ -218,20 +289,27 @@ def ram_write(self): self.core.break_realtime() self.dev.cpld.init() + if not self.io_update_device: + # Set MASK_NU to trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(True) self.dev.init() self.dev.set_cfr1(ram_enable=0) - self.dev.cpld.io_update.pulse_mu(8) + self.dev.io_update.pulse_mu(8) self.dev.set_profile_ram( start=0, end=0 + n - 1, step=1, profile=0, mode=RAM_MODE_RAMPUP) - self.dev.cpld.set_profile(0) - self.dev.cpld.io_update.pulse_mu(8) + self.dev.cpld.set_profile(0, 0) + self.dev.io_update.pulse_mu(8) delay(1*ms) self.dev.write_ram(write) delay(1*ms) self.dev.read_ram(read) self.set_dataset("w", write) self.set_dataset("r", read) + if not self.io_update_device: + self.core.break_realtime() + # Unset MASK_NU to un-trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(False) @kernel def ram_read_overlapping(self): @@ -243,9 +321,12 @@ def ram_read_overlapping(self): self.core.break_realtime() self.dev.cpld.init() + if not self.io_update_device: + # Set MASK_NU to trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(True) self.dev.init() self.dev.set_cfr1(ram_enable=0) - self.dev.cpld.io_update.pulse_mu(8) + self.dev.io_update.pulse_mu(8) self.dev.set_profile_ram( start=0, end=0 + len(write) - 1, step=1, @@ -254,13 +335,13 @@ def ram_read_overlapping(self): start=offset, end=offset + len(read) - 1, step=1, profile=1, mode=RAM_MODE_RAMPUP) - self.dev.cpld.set_profile(0) - self.dev.cpld.io_update.pulse_mu(8) + self.dev.cpld.set_profile(0, 0) + self.dev.io_update.pulse_mu(8) delay(1*ms) self.dev.write_ram(write) delay(1*ms) - self.dev.cpld.set_profile(1) - self.dev.cpld.io_update.pulse_mu(8) + self.dev.cpld.set_profile(0, 1) + self.dev.io_update.pulse_mu(8) self.dev.read_ram(read) # RAM profile addresses are apparently aligned @@ -270,15 +351,23 @@ def ram_read_overlapping(self): self.set_dataset("w", write[start:end]) self.set_dataset("r", read) + if not self.io_update_device: + self.core.break_realtime() + # Unset MASK_NU to un-trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(False) + @kernel def ram_exec(self): ftw0 = [0x12345678]*2 ftw1 = [0x55aaaa55]*2 self.core.break_realtime() self.dev.cpld.init() + if not self.io_update_device: + # Set MASK_NU to trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(True) self.dev.init() self.dev.set_cfr1(ram_enable=0) - self.dev.cpld.io_update.pulse_mu(8) + self.dev.io_update.pulse_mu(8) self.dev.set_profile_ram( start=100, end=100 + len(ftw0) - 1, step=1, @@ -287,28 +376,33 @@ def ram_exec(self): start=200, end=200 + len(ftw1) - 1, step=1, profile=4, mode=RAM_MODE_RAMPUP) - self.dev.cpld.set_profile(3) - self.dev.cpld.io_update.pulse_mu(8) + self.dev.cpld.set_profile(0, 3) + self.dev.io_update.pulse_mu(8) self.dev.write_ram(ftw0) - self.dev.cpld.set_profile(4) - self.dev.cpld.io_update.pulse_mu(8) + self.dev.cpld.set_profile(0, 4) + self.dev.io_update.pulse_mu(8) self.dev.write_ram(ftw1) self.dev.set_cfr1(ram_enable=1, ram_destination=RAM_DEST_FTW) - self.dev.cpld.io_update.pulse_mu(8) + self.dev.io_update.pulse_mu(8) - self.dev.cpld.set_profile(3) - self.dev.cpld.io_update.pulse_mu(8) + self.dev.cpld.set_profile(0, 3) + self.dev.io_update.pulse_mu(8) ftw0r = self.dev.read32(_AD9910_REG_FTW) delay(100*us) - self.dev.cpld.set_profile(4) - self.dev.cpld.io_update.pulse_mu(8) + self.dev.cpld.set_profile(0, 4) + self.dev.io_update.pulse_mu(8) ftw1r = self.dev.read32(_AD9910_REG_FTW) self.set_dataset("ftw", [ftw0[0], ftw0r, ftw1[0], ftw1r]) + if not self.io_update_device: + self.core.break_realtime() + # Unset MASK_NU to un-trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(False) + @kernel def ram_convert_frequency(self): freq = [33*MHz]*2 @@ -317,21 +411,28 @@ def ram_convert_frequency(self): self.core.break_realtime() self.dev.cpld.init() + if not self.io_update_device: + # Set MASK_NU to trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(True) self.dev.init() self.dev.set_cfr1(ram_enable=0) - self.dev.cpld.io_update.pulse_mu(8) + self.dev.io_update.pulse_mu(8) self.dev.set_profile_ram( start=100, end=100 + len(ram) - 1, step=1, profile=6, mode=RAM_MODE_RAMPUP) - self.dev.cpld.set_profile(6) - self.dev.cpld.io_update.pulse_mu(8) + self.dev.cpld.set_profile(0, 6) + self.dev.io_update.pulse_mu(8) self.dev.write_ram(ram) self.dev.set_cfr1(ram_enable=1, ram_destination=RAM_DEST_FTW) - self.dev.cpld.io_update.pulse_mu(8) + self.dev.io_update.pulse_mu(8) ftw_read = self.dev.read32(_AD9910_REG_FTW) self.set_dataset("ram", ram) self.set_dataset("ftw_read", ftw_read) self.set_dataset("freq", freq) + if not self.io_update_device: + self.core.break_realtime() + # Unset MASK_NU to un-trigger CFG.IO_UPDATE + self.dev.cfg_mask_nu(False) @kernel def ram_convert_powasf(self): @@ -348,59 +449,74 @@ class AD9910Test(ExperimentCase): def test_instantiate(self): self.execute(AD9910Exp, "instantiate") - def test_init(self): - self.execute(AD9910Exp, "init") + @io_update_device(True, False) + def test_init(self, io_update_device): + self.execute(AD9910Exp, "init", io_update_device=io_update_device) - def test_init_fail(self): + @io_update_device(True, False, proto_rev=STA_PROTO_REV_8) + def test_init_fail_proto_rev8(self, io_update_device): with self.assertRaises(ValueError): - self.execute(AD9910Exp, "init_fail") - - def test_set_get(self): - self.execute(AD9910Exp, "set_get") - for attr in ['ftw', 'pow', 'asf', 'att']: + self.execute( + AD9910Exp, "init_fail_proto_rev", io_update_device=io_update_device + ) + + @io_update_device(True, False) + def test_set_get(self, io_update_device): + self.execute(AD9910Exp, "set_get", io_update_device) + for attr in ["ftw", "pow", "asf", "att"]: with self.subTest(attribute=attr): get = self.dataset_mgr.get("{}_get".format(attr)) set_ = self.dataset_mgr.get("{}_set".format(attr)) self.assertEqual(get, set_) - def test_set_get_io_update_regs(self): - self.execute(AD9910Exp, "set_get_io_update_regs") - for attr in ['ftw', 'pow', 'asf']: + @io_update_device(True, False) + def test_set_get_io_update_regs(self, io_update_device): + self.execute( + AD9910Exp, "set_get_io_update_regs", io_update_device=io_update_device + ) + for attr in ["ftw", "pow", "asf"]: with self.subTest(attribute=attr): get = self.dataset_mgr.get("{}_get".format(attr)) set_ = self.dataset_mgr.get("{}_set".format(attr)) self.assertEqual(get, set_) - def test_read_write64(self): - self.execute(AD9910Exp, "read_write64") + @io_update_device(True, False) + def test_read_write64(self, io_update_device): + self.execute(AD9910Exp, "read_write64", io_update_device=io_update_device) write = self.dataset_mgr.get("write") read = self.dataset_mgr.get("read") self.assertEqual(hex(write), hex(read)) - def test_set_speed(self): - self.execute(AD9910Exp, "set_speed") + @io_update_device(True) + def test_set_speed(self, io_update_device): + self.execute(AD9910Exp, "set_speed", io_update_device=io_update_device) dt = self.dataset_mgr.get("dt") print(dt) - self.assertLess(dt, 70*us) + self.assertLess(dt, 70 * us) - def test_set_speed_mu(self): - self.execute(AD9910Exp, "set_speed_mu") + @io_update_device(True) + def test_set_speed_mu(self, io_update_device): + self.execute(AD9910Exp, "set_speed_mu", io_update_device=io_update_device) dt = self.dataset_mgr.get("dt") print(dt) - self.assertLess(dt, 11*us) - - def test_sync_window(self): - self.execute(AD9910Exp, "sync_window") - err = self.dataset_mgr.get("err") - dly = self.dataset_mgr.get("dly") - win = self.dataset_mgr.get("win") - print(dly, win, err) - # make sure one tap margin on either side of optimal delay - for i in -1, 0, 1: - self.assertEqual(err[i + dly], 0) - - def test_io_update_delay(self): - self.execute(AD9910Exp, "io_update_delay") + self.assertLess(dt, 11 * us) + + @io_update_device(True, False) + def test_sync_window(self, io_update_device): + # Assume sync_div is not zero + if "sync_device" in self.device_mgr.get_desc(DDS): + self.execute(AD9910Exp, "sync_window", io_update_device=io_update_device) + err = self.dataset_mgr.get("err") + dly = self.dataset_mgr.get("dly") + win = self.dataset_mgr.get("win") + print(dly, win, err) + # make sure one tap margin on either side of optimal delay + for i in -1, 0, 1: + self.assertEqual(err[i + dly], 0) + + @io_update_device(True) + def test_io_update_delay(self, io_update_device): + self.execute(AD9910Exp, "io_update_delay", io_update_device=io_update_device) dly = self.dataset_mgr.get("dly") bins1 = self.dataset_mgr.get("bins1") bins2 = self.dataset_mgr.get("bins2") @@ -409,53 +525,64 @@ def test_io_update_delay(self): # no edge at optimal delay self.assertEqual(bins2[(dly + 1) & 3], 0) # many edges near expected position - self.assertGreater(bins2[(dly + 3) & 3], n*.9) + self.assertGreater(bins2[(dly + 3) & 3], n * 0.9) - def test_sw_readback(self): - self.execute(AD9910Exp, "sw_readback") - self.assertEqual(self.dataset_mgr.get("sw"), (1, 0)) + @io_update_device(True, False) + def test_sw_readback(self, io_update_device): + if "sw" in self.device_mgr.get_desc(DDS): + self.execute(AD9910Exp, "sw_readback", io_update_device=io_update_device) + self.assertEqual(self.dataset_mgr.get("sw"), (1, 0)) - def test_profile_readback(self): - self.execute(AD9910Exp, "profile_readback") + @io_update_device(True, False) + def test_profile_readback(self, io_update_device): + self.execute(AD9910Exp, "profile_readback", io_update_device=io_update_device) self.assertEqual(self.dataset_mgr.get("ftw"), list(range(8))) - def test_ram_write(self): - self.execute(AD9910Exp, "ram_write") + @io_update_device(True, False) + def test_ram_write(self, io_update_device): + self.execute(AD9910Exp, "ram_write", io_update_device=io_update_device) read = self.dataset_mgr.get("r") write = self.dataset_mgr.get("w") self.assertEqual(len(read), len(write)) self.assertEqual(read, write) - def test_ram_read_overlapping(self): - self.execute(AD9910Exp, "ram_read_overlapping") + @io_update_device(True, False) + def test_ram_read_overlapping(self, io_update_device): + self.execute( + AD9910Exp, "ram_read_overlapping", io_update_device=io_update_device + ) read = self.dataset_mgr.get("r") write = self.dataset_mgr.get("w") self.assertEqual(len(read), 100) self.assertEqual(read, write) - def test_ram_exec(self): - self.execute(AD9910Exp, "ram_exec") + @io_update_device(True, False) + def test_ram_exec(self, io_update_device): + self.execute(AD9910Exp, "ram_exec", io_update_device=io_update_device) ftw = self.dataset_mgr.get("ftw") self.assertEqual(ftw[0], ftw[1]) self.assertEqual(ftw[2], ftw[3]) - def test_ram_convert_frequency(self): - exp = self.execute(AD9910Exp, "ram_convert_frequency") + @io_update_device(True, False) + def test_ram_convert_frequency(self, io_update_device): + exp = self.execute( + AD9910Exp, "ram_convert_frequency", io_update_device=io_update_device + ) ram = self.dataset_mgr.get("ram") ftw_read = self.dataset_mgr.get("ftw_read") self.assertEqual(ftw_read, ram[0]) freq = self.dataset_mgr.get("freq") self.assertEqual(ftw_read, exp.dev.frequency_to_ftw(freq[0])) - self.assertAlmostEqual(freq[0], exp.dev.ftw_to_frequency(ftw_read), - delta=.25) + self.assertAlmostEqual(freq[0], exp.dev.ftw_to_frequency(ftw_read), delta=0.25) - def test_ram_convert_powasf(self): - exp = self.execute(AD9910Exp, "ram_convert_powasf") + @io_update_device(True, False) + def test_ram_convert_powasf(self, io_update_device): + exp = self.execute( + AD9910Exp, "ram_convert_powasf", io_update_device=io_update_device + ) ram = self.dataset_mgr.get("ram") amplitude = self.dataset_mgr.get("amplitude") turns = self.dataset_mgr.get("turns") for i in range(len(ram)): - self.assertEqual((ram[i] >> 16) & 0xffff, - exp.dev.turns_to_pow(turns[i])) - self.assertEqual(ram[i] & 0xffff, - exp.dev.amplitude_to_asf(amplitude[i])) + self.assertEqual((ram[i] >> 16) & 0xFFFF, exp.dev.turns_to_pow(turns[i])) + self.assertEqual(ram[i] & 0xFFFF, exp.dev.amplitude_to_asf(amplitude[i])) diff --git a/artiq/test/coredevice/test_ad9910_waveform.py b/artiq/test/coredevice/test_ad9910_waveform.py new file mode 100644 index 0000000000..8c44145f62 --- /dev/null +++ b/artiq/test/coredevice/test_ad9910_waveform.py @@ -0,0 +1,582 @@ +from functools import wraps + +import artiq.coredevice.spi2 as spi +from artiq.coredevice.ad9910 import ( + _AD9910_REG_ASF, + _AD9910_REG_RAMP_LIMIT, + _AD9910_REG_RAMP_RATE, + _AD9910_REG_RAMP_STEP, +) +from artiq.coredevice.urukul import STA_PROTO_REV_8, STA_PROTO_REV_9 +from artiq.experiment import * +from artiq.test.hardware_testbench import ExperimentCase + +#################################################################################### +## NOTE: These test are intended to create waveforms from AD9910 Urukul boards +## (all protocol revisions, see tests below) used with an oscilloscope (2 channels). +## +## Set your oscilloscope to: +## +## Time division: 2 ns +## Voltage division: 500 mV +## +## The settings below work with a nominal 100 MHz waveform. Set trigger to DDS1. +## You will need to play with your trigger level setting for some of the tests. +## +## If you change FREQ (see below) to something else, adjust your oscilloscope +## settings to accomodate. +#################################################################################### + +FREQ = 100 * MHz +AMP = 1.0 +ATT = 1.0 + +# Set to desired devices +CPLD = "urukul1_cpld" +DDS1 = "urukul1_ch0" +DDS2 = "urukul1_ch3" + + +class AD9910WaveformExp(EnvExperiment): + def build( + self, + runner, + io_update_device=True, + multiple_profiles=True, + osk_manual=True, + drg_destination=0x0, + use_dds2=False, + with_hold=False, + nodwell=0, + ): + self.setattr_device("core") + self.cpld = self.get_device(CPLD) + self.dds1 = self.get_device(DDS1) + self.dds2 = self.get_device(DDS2) + self.runner = runner + self.io_update_device = io_update_device + self.multiple_profiles = multiple_profiles + self.osk_manual = osk_manual + self.drg_destination = drg_destination + self.use_dds2 = use_dds2 + self.with_hold = with_hold + self.nodwell = nodwell + + def run(self): + getattr(self, self.runner)() + + @kernel + def instantiate(self): + pass + + @kernel + def init(self): + self.core.break_realtime() + self.cpld.init() + if not self.io_update_device: + # Set MASK_NU to trigger CFG.IO_UPDATE + self.dds1.cfg_mask_nu(True) + self.dds2.cfg_mask_nu(True) + self.dds1.init() + self.dds2.init() + if not self.io_update_device: + # Unset MASK_NU to un-trigger CFG.IO_UPDATE + self.dds1.cfg_mask_nu(False) + self.dds2.cfg_mask_nu(False) + + @kernel + def single_tone(self): + self.core.reset() + self.cpld.init() + + # Set ATT_EN + if self.cpld.proto_rev == STA_PROTO_REV_9: + self.dds1.cfg_att_en(True) + self.dds2.cfg_att_en(True) + if not self.io_update_device: + # Set MASK_NU to trigger CFG.IO_UPDATE + self.dds1.cfg_mask_nu(True) + self.dds2.cfg_mask_nu(True) + + self.dds1.init() + self.dds2.init() + + delay(10 * ms) + + self.dds1.set(FREQ, amplitude=AMP) + self.dds2.set(FREQ, amplitude=AMP) + + # Switch on waveforms + self.dds1.cfg_sw(True) + self.dds2.cfg_sw(True) + + self.dds1.set_att(ATT) + self.dds2.set_att(ATT) + + delay(5 * s) + + # Switch off waveforms + self.dds1.cfg_sw(False) + self.dds2.cfg_sw(False) + + if not self.io_update_device: + # Unset MASK_NU to un-trigger CFG.IO_UPDATE + self.dds1.cfg_mask_nu(False) + self.dds2.cfg_mask_nu(False) + + # UnSet ATT_EN + if self.cpld.proto_rev == STA_PROTO_REV_9: + self.dds1.cfg_att_en(False) + self.dds2.cfg_att_en(False) + + self.core.wait_until_mu(now_mu()) + + @kernel + def toggle_profiles(self): + self.core.reset() + self.cpld.init() + # Set ATT_EN + self.dds1.cfg_att_en(True) + self.dds2.cfg_att_en(True) + if not self.io_update_device: + # Set MASK_NU to trigger CFG.IO_UPDATE + self.dds1.cfg_mask_nu(True) + self.dds2.cfg_mask_nu(True) + + self.dds1.init() + self.dds2.init() + + delay(10 * ms) + + ## SET SINGLE-TONE PROFILES + # Set profiles from 7 to 0 + frequencies = [ + 0.0, + 25 * MHz, + 50 * MHz, + 75 * MHz, + 100 * MHz, + 125 * MHz, + 150 * MHz, + 175 * MHz, + ] + for i in range(6, -1, -1): + freq_offset = frequencies[i] + profile = 7 - i + self.dds1.set(FREQ + freq_offset, amplitude=AMP, profile=profile) + self.dds2.set(FREQ + freq_offset, amplitude=AMP, profile=profile) + delay(0.5 * s) # slack + + # Switch on waveforms -- Profile 7 (default) + self.dds1.cfg_sw(True) + self.dds2.cfg_sw(True) + self.dds1.set_att(ATT) + self.dds2.set_att(ATT) + delay(2 * s) + # Switch off waveforms + self.dds1.cfg_sw(False) + self.dds2.cfg_sw(False) + + # Iterate over Profiles 6 to 0 + for i in range(6, -1, -1): + # Switch channels to Profile i + self.cpld.set_profile(0, i) + if self.multiple_profiles: + self.cpld.set_profile(3, i) + + self.dds1.cfg_sw(True) + self.dds2.cfg_sw(True) + delay(2 * s) + # Switch off waveforms + self.dds1.cfg_sw(False) + self.dds2.cfg_sw(False) + + if not self.io_update_device: + # Unset MASK_NU to un-trigger CFG.IO_UPDATE + self.dds1.cfg_mask_nu(False) + self.dds2.cfg_mask_nu(False) + + # UnSet ATT_EN + self.dds1.cfg_att_en(False) + self.dds2.cfg_att_en(False) + + self.core.wait_until_mu(now_mu()) + + @kernel + def osk(self): + self.core.reset() + self.cpld.init() + # Set ATT_EN + self.dds1.cfg_att_en(True) + if not self.io_update_device: + # Set MASK_NU to trigger CFG.IO_UPDATE + self.dds1.cfg_mask_nu(True) + + self.dds1.init() + + delay(10 * ms) + + self.dds1.set(FREQ, amplitude=AMP) + + if self.osk_manual: + self.dds1.set_cfr1(manual_osk_external=1, osk_enable=1) + else: + self.dds1.write32(_AD9910_REG_ASF, 0xFFFF << 16 | 0x3FFF << 2 | 0b11) + self.dds1.set_cfr1(osk_enable=1, select_auto_osk=1) + + self.dds1.io_update.pulse(1 * ms) + + # Switch on waveform, then set attenuation + self.dds1.cfg_sw(True) + self.dds1.set_att(ATT) + for _ in range(5): + # Toggle output via OSK + self.dds1.cfg_osk(True) + delay(1 * s) + self.dds1.cfg_osk(False) + delay(1 * s) + # Switch off waveform + self.dds1.cfg_sw(False) + + if not self.io_update_device: + # Unset MASK_NU to un-trigger CFG.IO_UPDATE + self.dds1.cfg_mask_nu(False) + + # UnSet ATT_EN + self.dds1.cfg_att_en(False) + + self.core.wait_until_mu(now_mu()) + + @kernel + def drg(self): + self.core.reset() + self.cpld.init() + + # Set ATT_EN + self.dds1.cfg_att_en(True) + if self.use_dds2: + self.dds2.cfg_att_en(True) + + if not self.io_update_device: + # Set MASK_NU to trigger CFG.IO_UPDATE + self.dds1.cfg_mask_nu(True) + if self.use_dds2: + self.dds2.cfg_mask_nu(True) + + self.dds1.init() + if self.use_dds2: + self.dds2.init() + + delay(10 * ms) + + # Set initial frequency and amplitude + self.dds1.set(FREQ, amplitude=AMP) + if self.use_dds2: + self.dds2.set(FREQ, amplitude=AMP) + + # Configure DRG + if self.drg_destination == 0x0: # Frequency + ramp_limit_high = self.dds1.frequency_to_ftw(FREQ + 30 * MHz) + ramp_limit_low = self.dds1.frequency_to_ftw(FREQ - 30 * MHz) + ramp_rate = 0x004F004F + ramp_step = 0xF0 + pulse_duration = 1 * s if not self.with_hold else 0.25 * s + elif self.drg_destination == 0x1: # Phase + ramp_limit_high = 0xFFFFFFFF + ramp_limit_low = 0 + ramp_rate = 0xC350C350 + ramp_step = 0xD1B71 + pulse_duration = 2 * s if not self.with_hold else 0.25 * s + else: # Amplitude + ramp_limit_high = 0xFFFFFFFF + ramp_limit_low = 0 + ramp_rate = 0xC350C350 + ramp_step = 0xD1B71 + pulse_duration = 2 * s if not self.with_hold else 0.4 * s + + self.dds1.write64(_AD9910_REG_RAMP_LIMIT, ramp_limit_high, ramp_limit_low) + self.dds1.write32(_AD9910_REG_RAMP_RATE, ramp_rate) + self.dds1.write64(_AD9910_REG_RAMP_STEP, ramp_step, ramp_step) + + self.dds1.set_cfr2( + drg_enable=1, + drg_destination=self.drg_destination, + drg_nodwell_high=self.nodwell, + drg_nodwell_low=self.nodwell, + ) + self.dds1.io_update.pulse(1 * ms) + + # Enable waveform and set attenuation + self.dds1.cfg_sw(True) + self.dds1.set_att(ATT) + if self.use_dds2: + self.dds2.cfg_sw(True) + self.dds2.set_att(ATT) + + if not self.nodwell: + for _ in range(5): + if self.with_hold: + self.dds1.cfg_drctl(True) + delay(pulse_duration) + self.dds1.cfg_drhold(True) + delay(pulse_duration) + self.dds1.cfg_drhold(False) + delay(pulse_duration) + self.dds1.cfg_drctl(False) + delay(pulse_duration) + self.dds1.cfg_drhold(True) + delay(pulse_duration) + self.dds1.cfg_drhold(False) + delay(pulse_duration) + else: + self.dds1.cfg_drctl(True) + delay(pulse_duration) + self.dds1.cfg_drctl(False) + delay(pulse_duration) + else: + delay(10 * s) + + # Disable waveform + self.dds1.cfg_sw(False) + if self.use_dds2: + self.dds2.cfg_sw(False) + + if not self.io_update_device: + # Unset MASK_NU to un-trigger CFG.IO_UPDATE + self.dds1.cfg_mask_nu(False) + if self.use_dds2: + self.dds2.cfg_mask_nu(False) + + # Unset ATT_EN + self.dds1.cfg_att_en(False) + if self.use_dds2: + self.dds2.cfg_att_en(False) + + self.core.wait_until_mu(now_mu()) + + @kernel + def att_single_tone(self): + self.core.reset() + self.cpld.init() + # Set ATT_EN + self.dds1.cfg_att_en(True) + self.dds2.cfg_att_en(True) + if not self.io_update_device: + # Set MASK_NU to trigger CFG.IO_UPDATE + self.dds1.cfg_mask_nu(True) + self.dds2.cfg_mask_nu(True) + + self.dds1.init() + self.dds2.init() + + delay(10 * ms) + + self.dds1.set(FREQ, amplitude=AMP) + self.dds2.set(FREQ, amplitude=AMP) + + delay(1 * ms) + + # Switch on waveforms + self.dds1.cfg_sw(True) + self.dds2.cfg_sw(True) + + # This must be set AFTER cfg_sw set to True (why?) + self.dds1.set_att(0.0 * dB) + self.dds2.set_att(0.0 * dB) + + delay(5 * s) + + self.dds1.set_att(10.0) + self.dds2.set_att(10.0) + + delay(5 * s) + + self.dds1.set_att(20.0) + self.dds2.set_att(20.0) + + delay(5 * s) + + self.dds1.set_att(30.0) + self.dds2.set_att(30.0) + + delay(5 * s) + + # Switch off waveforms + self.dds1.cfg_sw(False) + self.dds2.cfg_sw(False) + + if not self.io_update_device: + # Unset MASK_NU to un-trigger CFG.IO_UPDATE + self.dds1.cfg_mask_nu(False) + self.dds2.cfg_mask_nu(False) + + # UnSet ATT_EN + self.dds1.cfg_att_en(False) + self.dds2.cfg_att_en(False) + + self.core.wait_until_mu(now_mu()) + + +def io_update_device(*required_values, proto_rev=None): + """ + Decorator to mark whether a test requires 'io_update_device' to be set or unset + and optionally check the protocol version. + """ + + def decorator(test_func): + @wraps(test_func) + def wrapper(self, *args, **kwargs): + for required in required_values: + with self.subTest(io_update_device=required): + desc = ( + self.device_mgr.get_desc(CPLD) + if hasattr(self.device_mgr, "get_desc") + else {} + ) + io_update_present = "io_update_device" in desc.get("arguments", []) + + if io_update_present != required: + self.skipTest( + f"This test requires 'io_update_device' to be {required}." + ) + + if proto_rev is not None: + actual_proto_rev = self.device_mgr.get(CPLD).proto_rev + if actual_proto_rev != proto_rev: + self.skipTest( + f"This test requires proto_rev={proto_rev}, " + "but the current proto_rev is {actual_proto_rev}." + ) + + print( + f"Running test: {test_func.__name__} (io_update_device={required})" + ) + test_func(self, *args, **kwargs, io_update_device=required) + + return wrapper + + return decorator + + +class AD9910WaveformTest(ExperimentCase): + def test_instantiate(self): + self.execute(AD9910WaveformExp, "instantiate") + + @io_update_device(True, False) + def test_init(self, io_update_device): + self.execute(AD9910WaveformExp, "init", io_update_device=io_update_device) + + @io_update_device(True, False) + def test_single_tone(self, io_update_device): + self.execute( + AD9910WaveformExp, "single_tone", io_update_device=io_update_device + ) + + @io_update_device(True, False, proto_rev=STA_PROTO_REV_8) + def test_toggle_profiles(self, io_update_device): + self.execute( + AD9910WaveformExp, + "toggle_profiles", + io_update_device=io_update_device, + multiple_profiles=False, + ) + + @io_update_device(True, False, proto_rev=STA_PROTO_REV_9) + def test_toggle_profiles(self, io_update_device): + self.execute( + AD9910WaveformExp, + "toggle_profiles", + io_update_device=io_update_device, + ) + + @io_update_device(True, False, proto_rev=STA_PROTO_REV_9) + def test_osk_manual(self, io_update_device): + self.execute(AD9910WaveformExp, "osk", io_update_device=io_update_device) + + @io_update_device(True, False, proto_rev=STA_PROTO_REV_9) + def test_osk_auto(self, io_update_device): + self.execute( + AD9910WaveformExp, + "osk", + io_update_device=io_update_device, + osk_manual=False, + ) + + @io_update_device(True, False, proto_rev=STA_PROTO_REV_9) + def test_drg_normal_frequency(self, io_update_device): + self.execute(AD9910WaveformExp, "drg", io_update_device=io_update_device) + + @io_update_device(True, False, proto_rev=STA_PROTO_REV_9) + def test_drg_normal_phase(self, io_update_device): + self.execute( + AD9910WaveformExp, + "drg", + io_update_device=io_update_device, + drg_destination=0x1, + use_dds2=True, + ) + + @io_update_device(True, False, proto_rev=STA_PROTO_REV_9) + def test_drg_normal_amplitude(self, io_update_device): + self.execute( + AD9910WaveformExp, + "drg", + io_update_device=io_update_device, + drg_destination=0x2, + ) + + @io_update_device(True, False, proto_rev=STA_PROTO_REV_9) + def test_drg_normal_with_hold_frequency(self, io_update_device): + self.execute( + AD9910WaveformExp, "drg", io_update_device=io_update_device, with_hold=True + ) + + @io_update_device(True, False, proto_rev=STA_PROTO_REV_9) + def test_drg_normal_with_hold_phase(self, io_update_device): + self.execute( + AD9910WaveformExp, + "drg", + io_update_device=io_update_device, + drg_destination=0x1, + use_dds2=True, + with_hold=True, + ) + + @io_update_device(True, False, proto_rev=STA_PROTO_REV_9) + def test_drg_normal_with_hold_amplitude(self, io_update_device): + self.execute( + AD9910WaveformExp, + "drg", + io_update_device=io_update_device, + drg_destination=0x2, + with_hold=True, + ) + + @io_update_device(True, False, proto_rev=STA_PROTO_REV_9) + def test_drg_nodwell_frequency(self, io_update_device): + self.execute( + AD9910WaveformExp, "drg", io_update_device=io_update_device, nodwell=1 + ) + + @io_update_device(True, False, proto_rev=STA_PROTO_REV_9) + def test_drg_nodwell_phase(self, io_update_device): + self.execute( + AD9910WaveformExp, + "drg", + io_update_device=io_update_device, + drg_destination=0x1, + use_dds2=True, + nodwell=1, + ) + + @io_update_device(True, False, proto_rev=STA_PROTO_REV_9) + def test_drg_nodwell_amplitude(self, io_update_device): + self.execute( + AD9910WaveformExp, "drg", io_update_device, drg_destination=0x2, nodwell=1 + ) + + @io_update_device(True, False, proto_rev=STA_PROTO_REV_9) + def test_att_single_tone(self, io_update_device): + self.execute( + AD9910WaveformExp, "att_single_tone", io_update_device=io_update_device + ) diff --git a/artiq/test/coredevice/test_urukul.py b/artiq/test/coredevice/test_urukul.py index 8cfe453f8d..907e9d7a2e 100644 --- a/artiq/test/coredevice/test_urukul.py +++ b/artiq/test/coredevice/test_urukul.py @@ -1,12 +1,14 @@ +from artiq.coredevice.urukul import STA_PROTO_REV_9, urukul_sta_rf_sw from artiq.experiment import * from artiq.test.hardware_testbench import ExperimentCase -from artiq.coredevice import urukul + +CPLD = "urukul_cpld" class UrukulExp(EnvExperiment): def build(self, runner): self.setattr_device("core") - self.dev = self.get_device("urukul_cpld") + self.dev = self.get_device(CPLD) self.runner = runner def run(self): @@ -35,12 +37,17 @@ def sta_read(self): self.set_dataset("sta", sta) @kernel - def switches(self): + def io_rst(self): self.core.break_realtime() self.dev.init() self.dev.io_rst() + + @kernel + def switches(self): + self.core.break_realtime() + self.dev.init() self.dev.cfg_sw(0, False) - self.dev.cfg_sw(0, True) + self.dev.cfg_sw(1, True) self.dev.cfg_sw(3, True) self.dev.cfg_switches(0b1010) @@ -51,7 +58,7 @@ def switch_speed(self): n = 10 t0 = self.core.get_rtio_counter_mu() for i in range(n): - self.dev.cfg_sw(3, bool(i & 1)) + self.dev.cfg_sw(2, bool(i & 1)) self.set_dataset("dt", self.core.mu_to_seconds( self.core.get_rtio_counter_mu() - t0) / n) @@ -65,11 +72,30 @@ def switches_readback(self): self.set_dataset("sw_set", sw_set) self.set_dataset("sta_get", sta_get) + @kernel + def att_enables(self): + self.core.break_realtime() + self.dev.init() + self.dev.cfg_att_en(0, False) + self.dev.cfg_att_en(2, True) + self.dev.cfg_att_en(3, True) + self.dev.cfg_att_en_all(0b1010) + + @kernel + def att_enable_speed(self): + self.core.break_realtime() + self.dev.init() + n = 10 + t0 = self.core.get_rtio_counter_mu() + for i in range(n): + self.dev.cfg_att_en(1, bool(i & 1)) + self.set_dataset("dt", self.core.mu_to_seconds( + self.core.get_rtio_counter_mu() - t0) / n) + @kernel def att(self): self.core.break_realtime() self.dev.init() - # clear backing state self.dev.att_reg = 0 att_set = 0x12345678 self.dev.set_all_att_mu(att_set) @@ -107,7 +133,7 @@ def att_channel_get(self): self.dev.init() # clear backing state self.dev.att_reg = 0 - att_set = [int32(0x21), int32(0x43), + att_set = [int32(0x21), int32(0x43), int32(0x65), int32(0x87)] # set individual attenuators for i in range(len(att_set)): @@ -135,10 +161,84 @@ def att_speed(self): self.core.get_rtio_counter_mu() - t0) / n) @kernel - def io_update(self): + def osk(self): + self.core.break_realtime() + self.dev.init() + self.dev.cfg_osk(0, False) + self.dev.cfg_osk(2, True) + self.dev.cfg_osk(3, True) + self.dev.cfg_osk_all(0b1010) + + @kernel + def osk_speed(self): + self.core.break_realtime() + self.dev.init() + n = 10 + t0 = self.core.get_rtio_counter_mu() + for i in range(n): + self.dev.cfg_osk(1, bool(i & 1)) + self.set_dataset("dt", self.core.mu_to_seconds( + self.core.get_rtio_counter_mu() - t0) / n) + + @kernel + def drctl(self): + self.core.break_realtime() + self.dev.init() + self.dev.cfg_drctl(0, False) + self.dev.cfg_drctl(1, True) + self.dev.cfg_drctl(3, True) + self.dev.cfg_drctl_all(0b1010) + + @kernel + def drctl_speed(self): + self.core.break_realtime() + self.dev.init() + n = 10 + t0 = self.core.get_rtio_counter_mu() + for i in range(n): + self.dev.cfg_drctl(2, bool(i & 1)) + self.set_dataset("dt", self.core.mu_to_seconds( + self.core.get_rtio_counter_mu() - t0) / n) + + @kernel + def drhold(self): + self.core.break_realtime() + self.dev.init() + self.dev.cfg_drhold(0, False) + self.dev.cfg_drhold(2, True) + self.dev.cfg_drhold(3, True) + self.dev.cfg_drhold_all(0b1010) + + @kernel + def drhold_speed(self): + self.core.break_realtime() + self.dev.init() + n = 10 + t0 = self.core.get_rtio_counter_mu() + for i in range(n): + self.dev.cfg_drhold(1, bool(i & 1)) + self.set_dataset("dt", self.core.mu_to_seconds( + self.core.get_rtio_counter_mu() - t0) / n) + + @kernel + def mask_nu(self): self.core.break_realtime() self.dev.init() - self.dev.io_update.pulse_mu(8) + self.dev.cfg_mask_nu(0, False) + self.dev.cfg_mask_nu(1, True) + self.dev.cfg_mask_nu(3, True) + self.dev.cfg_mask_nu_all(0b1010) + + @kernel + def mask_nu_speed(self): + self.core.break_realtime() + self.dev.init() + n = 10 + t0 = self.core.get_rtio_counter_mu() + for i in range(n): + self.dev.cfg_mask_nu(2, bool(i & 1)) + self.set_dataset("dt", self.core.mu_to_seconds( + self.core.get_rtio_counter_mu() - t0) / n) @kernel def sync(self): @@ -150,8 +250,18 @@ def sync(self): def profile(self): self.core.break_realtime() self.dev.init() - self.dev.set_profile(7) - self.dev.set_profile(0) + self.dev.set_profile(0, 7) + self.dev.set_profile(0, 0) + + @kernel + def cfg_profile(self): + self.core.break_realtime() + self.dev.init() + self.dev.set_profile(0, 7) + self.dev.set_profile(1, 0) + self.dev.set_profile(2, 3) + self.dev.set_profile(3, 5) + self.dev.cfg_drctl_all(0b1111) class UrukulTest(ExperimentCase): @@ -168,7 +278,9 @@ def test_sta_read(self): self.execute(UrukulExp, "sta_read") sta = self.dataset_mgr.get("sta") print(hex(sta)) - # self.assertEqual(urukul.urukul_sta_ifc_mode(sta), 0b0001) + + def test_io_rst(self): + self.execute(UrukulExp, "io_rst") def test_switches(self): self.execute(UrukulExp, "switches") @@ -181,10 +293,21 @@ def test_switch_speed(self): def test_switches_readback(self): self.execute(UrukulExp, "switches_readback") - sw_get = urukul.urukul_sta_rf_sw(self.dataset_mgr.get("sta_get")) + sw_get = urukul_sta_rf_sw(self.dataset_mgr.get("sta_get")) sw_set = self.dataset_mgr.get("sw_set") self.assertEqual(sw_get, sw_set) + def test_att_enables(self): + if self.device_mgr.get(CPLD).proto_rev == STA_PROTO_REV_9: + self.execute(UrukulExp, "att_enables") + + def test_att_enable_speed(self): + if self.device_mgr.get(CPLD).proto_rev == STA_PROTO_REV_9: + self.execute(UrukulExp, "att_enable_speed") + dt = self.dataset_mgr.get("dt") + print(dt) + self.assertLess(dt, 5 * us) + def test_att(self): self.execute(UrukulExp, "att") att_set = self.dataset_mgr.get("att_set") @@ -212,8 +335,49 @@ def test_att_speed(self): print(dt) self.assertLess(dt, 5 * us) - def test_io_update(self): - self.execute(UrukulExp, "io_update") + def test_osk(self): + if self.device_mgr.get(CPLD).proto_rev == STA_PROTO_REV_9: + self.execute(UrukulExp, "osk") + + def test_osk_speed(self): + if self.device_mgr.get(CPLD).proto_rev == STA_PROTO_REV_9: + self.execute(UrukulExp, "osk_speed") + dt = self.dataset_mgr.get("dt") + print(dt) + self.assertLess(dt, 5 * us) + + def test_drctl(self): + if self.device_mgr.get(CPLD).proto_rev == STA_PROTO_REV_9: + self.execute(UrukulExp, "drctl") + + def test_drctl_speed(self): + if self.device_mgr.get(CPLD).proto_rev == STA_PROTO_REV_9: + self.execute(UrukulExp, "drctl_speed") + dt = self.dataset_mgr.get("dt") + print(dt) + self.assertLess(dt, 5 * us) + + def test_drhold(self): + if self.device_mgr.get(CPLD).proto_rev == STA_PROTO_REV_9: + self.execute(UrukulExp, "drhold") + + def test_drhold_speed(self): + if self.device_mgr.get(CPLD).proto_rev == STA_PROTO_REV_9: + self.execute(UrukulExp, "drhold_speed") + dt = self.dataset_mgr.get("dt") + print(dt) + self.assertLess(dt, 5 * us) + + def test_mask_nu(self): + if self.device_mgr.get(CPLD).proto_rev == STA_PROTO_REV_9: + self.execute(UrukulExp, "mask_nu") + + def test_mask_nu_speed(self): + if self.device_mgr.get(CPLD).proto_rev == STA_PROTO_REV_9: + self.execute(UrukulExp, "mask_nu_speed") + dt = self.dataset_mgr.get("dt") + print(dt) + self.assertLess(dt, 5 * us) def test_sync(self): self.execute(UrukulExp, "sync")