diff --git a/artiq/coredevice/ad9910.py b/artiq/coredevice/ad9910.py index c7e1b49a1..9bdfd37fb 100644 --- a/artiq/coredevice/ad9910.py +++ b/artiq/coredevice/ad9910.py @@ -1,13 +1,27 @@ from numpy import int32, int64 -from artiq.language.core import kernel, delay, portable -from artiq.language.units import us, ns, ms +from artiq.language.core import ( + kernel, delay, portable, delay_mu, now_mu, at_mu) +from artiq.language.units import us, ms from artiq.coredevice import spi2 as spi from artiq.coredevice import urukul +# Work around ARTIQ-Python import machinery urukul_sta_pll_lock = urukul.urukul_sta_pll_lock +urukul_sta_smp_err = urukul.urukul_sta_smp_err +__all__ = [ + "AD9910", + "PHASE_MODE_CONTINUOUS", "PHASE_MODE_ABSOLUTE", "PHASE_MODE_TRACKING" +] + + +_PHASE_MODE_DEFAULT = -1 +PHASE_MODE_CONTINUOUS = 0 +PHASE_MODE_ABSOLUTE = 1 +PHASE_MODE_TRACKING = 2 + _AD9910_REG_CFR1 = 0x00 _AD9910_REG_CFR2 = 0x01 _AD9910_REG_CFR3 = 0x02 @@ -49,12 +63,21 @@ class AD9910: Urukul CPLD instance). :param pll_cp: DDS PLL charge pump setting. :param pll_vco: DDS PLL VCO range selection. + :param sync_delay_seed: SYNC_IN delay tuning starting value. + To stabilize the SYNC_IN delay tuning, run :meth:`tune_sync_delay` once + and set this to the delay tap number returned (default: -1 to signal no + synchronization and no tuning during :meth:`init`). + :param io_update_delay: IO_UPDATE pulse alignment delay. + To align IO_UPDATE to SYNC_CLK, run :meth:`tune_io_update_delay` and + set this to the delay tap number returned. """ kernel_invariants = {"chip_select", "cpld", "core", "bus", - "ftw_per_hz", "pll_n", "pll_cp", "pll_vco"} + "ftw_per_hz", "pll_n", "io_update_delay", + "sysclk_per_mu"} def __init__(self, dmgr, chip_select, cpld_device, sw_device=None, - pll_n=40, pll_cp=7, pll_vco=5): + pll_n=40, pll_cp=7, pll_vco=5, sync_delay_seed=-1, + io_update_delay=0): self.cpld = dmgr.get(cpld_device) self.core = self.cpld.core self.bus = self.cpld.bus @@ -68,14 +91,64 @@ class AD9910: assert self.cpld.refclk/4 <= 60e6 sysclk = self.cpld.refclk*pll_n/4 # Urukul clock fanout divider assert sysclk <= 1e9 - self.ftw_per_hz = 1./sysclk*(int64(1) << 32) + self.ftw_per_hz = (1 << 32)/sysclk + self.sysclk_per_mu = int(round(sysclk*self.core.ref_period)) + assert self.sysclk_per_mu == sysclk*self.core.ref_period assert 0 <= pll_vco <= 5 vco_min, vco_max = [(370, 510), (420, 590), (500, 700), - (600, 880), (700, 950), (820, 1150)][pll_vco] + (600, 880), (700, 950), (820, 1150)][pll_vco] assert vco_min <= sysclk/1e6 <= vco_max self.pll_vco = pll_vco assert 0 <= pll_cp <= 7 self.pll_cp = pll_cp + self.sync_delay_seed = sync_delay_seed + self.io_update_delay = io_update_delay + self.phase_mode = PHASE_MODE_CONTINUOUS + + @kernel + def set_phase_mode(self, phase_mode): + """Set the default phase mode. + + for future calls to :meth:`set` and + :meth:`set_mu`. Supported phase modes are: + + * :const:`PHASE_MODE_CONTINUOUS`: the phase accumulator is unchanged + when changing frequency or phase. The DDS phase is the sum of the + phase accumulator and the phase offset. The only discontinuous + changes in the DDS output phase come from changes to the phase + offset. This mode is also knows as "relative phase mode". + :math:`\phi(t) = q(t^\prime) + p + (t - t^\prime) f` + + * :const:`PHASE_MODE_ABSOLUTE`: the phase accumulator is reset when + changing frequency or phase. Thus, the phase of the DDS at the + time of the change is equal to the specified phase offset. + :math:`\phi(t) = p + (t - t^\prime) f` + + * :const:`PHASE_MODE_TRACKING`: when changing frequency or phase, + the phase accumulator is cleared and the phase offset is offset + by the value the phase accumulator would have if the DDS had been + running at the specified frequency since a given fiducial + time stamp. This is functionally equivalent to + :const:`PHASE_MODE_ABSOLUTE`. The only difference is the fiducial + time stamp. This mode is also known as "coherent phase mode". + The default fiducial time stamp is 0. + :math:`\phi(t) = p + (t - T) f` + + Where: + + * :math:`\phi(t)`: the DDS output phase + * :math:`q(t) = \phi(t) - p`: DDS internal phase accumulator + * :math:`p`: phase offset + * :math:`f`: frequency + * :math:`t^\prime`: time stamp of setting :math:`p`, :math:`f` + * :math:`T`: fiducial time stamp + * :math:`t`: running time + + .. warning:: This setting may become inconsistent when used as part of + a DMA recording. When using DMA, it is recommended to specify the + phase mode explicitly when calling :meth:`set` or :meth:`set_mu`. + """ + self.phase_mode = phase_mode @kernel def write32(self, addr, data): @@ -85,10 +158,10 @@ class AD9910: :param data: Data to be written """ self.bus.set_config_mu(urukul.SPI_CONFIG, 8, - urukul.SPIT_DDS_WR, self.chip_select) + urukul.SPIT_DDS_WR, self.chip_select) self.bus.write(addr << 24) self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 32, - urukul.SPIT_DDS_WR, self.chip_select) + urukul.SPIT_DDS_WR, self.chip_select) self.bus.write(data) @kernel @@ -98,11 +171,11 @@ class AD9910: :param addr: Register address """ self.bus.set_config_mu(urukul.SPI_CONFIG, 8, - urukul.SPIT_DDS_WR, self.chip_select) + urukul.SPIT_DDS_WR, self.chip_select) self.bus.write((addr | 0x80) << 24) - self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END - | spi.SPI_INPUT, 32, - urukul.SPIT_DDS_RD, self.chip_select) + self.bus.set_config_mu( + urukul.SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, + 32, urukul.SPIT_DDS_RD, self.chip_select) self.bus.write(0) return self.bus.read() @@ -115,13 +188,13 @@ class AD9910: :param data_low: Low (LSB) 32 data bits """ self.bus.set_config_mu(urukul.SPI_CONFIG, 8, - urukul.SPIT_DDS_WR, self.chip_select) + urukul.SPIT_DDS_WR, self.chip_select) self.bus.write(addr << 24) self.bus.set_config_mu(urukul.SPI_CONFIG, 32, - urukul.SPIT_DDS_WR, self.chip_select) + urukul.SPIT_DDS_WR, self.chip_select) self.bus.write(data_high) self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 32, - urukul.SPIT_DDS_WR, self.chip_select) + urukul.SPIT_DDS_WR, self.chip_select) self.bus.write(data_low) @kernel @@ -136,7 +209,7 @@ class AD9910: """ # Set SPI mode self.write32(_AD9910_REG_CFR1, 0x00000002) - self.cpld.io_update.pulse(2*us) + self.cpld.io_update.pulse(1*us) delay(1*ms) if not blind: # Use the AUX DAC setting to identify and confirm presence @@ -145,60 +218,121 @@ class AD9910: raise ValueError("Urukul AD9910 AUX_DAC mismatch") delay(50*us) # slack # Configure PLL settings and bring up PLL - self.write32(_AD9910_REG_CFR2, 0x01400020) - self.cpld.io_update.pulse(2*us) + # enable amplitude scale from profiles + # read effective FTW + # sync timing validation disable (enabled later) + self.write32(_AD9910_REG_CFR2, 0x01010020) + self.cpld.io_update.pulse(1*us) cfr3 = (0x0807c100 | (self.pll_vco << 24) | (self.pll_cp << 19) | (self.pll_n << 1)) self.write32(_AD9910_REG_CFR3, cfr3 | 0x400) # PFD reset - self.cpld.io_update.pulse(100*us) + self.cpld.io_update.pulse(1*us) self.write32(_AD9910_REG_CFR3, cfr3) - self.cpld.io_update.pulse(100*us) + self.cpld.io_update.pulse(1*us) if blind: delay(100*ms) - return - # Wait for PLL lock, up to 100 ms - for i in range(100): - sta = self.cpld.sta_read() - lock = urukul_sta_pll_lock(sta) - delay(1*ms) - if lock & (1 << self.chip_select - 4): - return - raise ValueError("PLL lock timeout") + else: + # Wait for PLL lock, up to 100 ms + for i in range(100): + sta = self.cpld.sta_read() + lock = urukul_sta_pll_lock(sta) + delay(1*ms) + if lock & (1 << self.chip_select - 4): + break + if i >= 100 - 1: + raise ValueError("PLL lock timeout") + if self.sync_delay_seed >= 0: + self.tune_sync_delay(self.sync_delay_seed) + delay(1*ms) @kernel - def set_mu(self, ftw, pow=0, asf=0x3fff): + def power_down(self, bits=0b1111): + """Power down DDS. + + :param bits: power down bits, see datasheet + """ + self.write32(_AD9910_REG_CFR1, 0x00000002 | (bits << 4)) + self.cpld.io_update.pulse(1*us) + + @kernel + def set_mu(self, ftw, pow=0, asf=0x3fff, phase_mode=_PHASE_MODE_DEFAULT, + ref_time=-1): """Set profile 0 data in machine units. + This uses machine units (FTW, POW, ASF). The frequency tuning word + width is 32, the phase offset word width is 16, and the amplitude + scale factor width is 12. + After the SPI transfer, the shared IO update pin is pulsed to activate the data. + .. seealso: :meth:`set_phase_mode` for a definition of the different + phase modes. + :param ftw: Frequency tuning word: 32 bit. :param pow: Phase tuning word: 16 bit unsigned. :param asf: Amplitude scale factor: 14 bit unsigned. + :param phase_mode: If specified, overrides the default phase mode set + by :meth:`set_phase_mode` for this call. + :param ref_time: Fiducial time used to compute absolute or tracking + phase updates. In machine units as obtained by `now_mu()`. + :return: Resulting phase offset word after application of phase + tracking offset. When using :const:`PHASE_MODE_CONTINUOUS` in + subsequent calls, use this value as the "current" phase. """ + if phase_mode == _PHASE_MODE_DEFAULT: + phase_mode = self.phase_mode + # Align to coarse RTIO which aligns SYNC_CLK + at_mu(now_mu() & ~0xf) + if phase_mode != PHASE_MODE_CONTINUOUS: + # Auto-clear phase accumulator on IO_UPDATE. + # This is active already for the next IO_UPDATE + self.write32(_AD9910_REG_CFR1, 0x00002002) + if phase_mode == PHASE_MODE_TRACKING and ref_time < 0: + # set default fiducial time stamp + ref_time = 0 + if ref_time >= 0: + # 32 LSB are sufficient. + # Also no need to use IO_UPDATE time as this + # is equivalent to an output pipeline latency. + dt = int32(now_mu()) - int32(ref_time) + pow += dt*ftw*self.sysclk_per_mu >> 16 self.write64(_AD9910_REG_PR0, (asf << 16) | pow, ftw) - self.cpld.io_update.pulse(10*ns) + delay_mu(int64(self.io_update_delay)) + self.cpld.io_update.pulse_mu(8) # assumes 8 mu > t_SYSCLK + at_mu(now_mu() & ~0xf) + if phase_mode != PHASE_MODE_CONTINUOUS: + self.write32(_AD9910_REG_CFR1, 0x00000002) + # future IO_UPDATE will activate + return pow @portable(flags={"fast-math"}) def frequency_to_ftw(self, frequency): - """Returns the frequency tuning word corresponding to the given + """Return the frequency tuning word corresponding to the given frequency. """ return int32(round(self.ftw_per_hz*frequency)) @portable(flags={"fast-math"}) def turns_to_pow(self, turns): - """Returns the phase offset word corresponding to the given phase + """Return the phase offset word corresponding to the given phase in turns.""" return int32(round(turns*0x10000)) @portable(flags={"fast-math"}) def amplitude_to_asf(self, amplitude): - """Returns amplitude scale factor corresponding to given amplitude.""" + """Return amplitude scale factor corresponding to given amplitude.""" return int32(round(amplitude*0x3ffe)) + @portable(flags={"fast-math"}) + def pow_to_turns(self, pow): + """Return the phase in turns corresponding to a given phase offset + word.""" + return pow/0x10000 + @kernel - def set(self, frequency, phase=0.0, amplitude=1.0): + def set(self, frequency, phase=0.0, amplitude=1.0, + phase_mode=_PHASE_MODE_DEFAULT, ref_time=-1): """Set profile 0 data in SI units. .. seealso:: :meth:`set_mu` @@ -206,10 +340,13 @@ class AD9910: :param ftw: Frequency in Hz :param pow: Phase tuning word in turns :param asf: Amplitude in units of full scale + :param phase_mode: Phase mode constant + :param ref_time: Fiducial time stamp in machine units + :return: Resulting phase offset in turns """ - self.set_mu(self.frequency_to_ftw(frequency), - self.turns_to_pow(phase), - self.amplitude_to_asf(amplitude)) + return self.pow_to_turns(self.set_mu( + self.frequency_to_ftw(frequency), self.turns_to_pow(phase), + self.amplitude_to_asf(amplitude), phase_mode, ref_time)) @kernel def set_att_mu(self, att): @@ -240,3 +377,148 @@ class AD9910: :param state: CPLD CFG RF switch bit """ self.cpld.cfg_sw(self.chip_select - 4, state) + + @kernel + def set_sync(self, in_delay, window): + """Set the relevant parameters in the multi device synchronization + register. See the AD9910 datasheet for details. The SYNC clock + generator preset value is set to zero, and the SYNC_OUT generator is + disabled. + + :param in_delay: SYNC_IN delay tap (0-31) in steps of ~75ps + :param window: Symmetric SYNC_IN validation window (0-15) in + steps of ~75ps for both hold and setup margin. + """ + self.write32(_AD9910_REG_MSYNC, + (window << 28) | # SYNC S/H validation delay + (1 << 27) | # SYNC receiver enable + (0 << 26) | # SYNC generator disable + (0 << 25) | # SYNC generator SYS rising edge + (0 << 18) | # SYNC preset + (0 << 11) | # SYNC output delay + (in_delay << 3)) # SYNC receiver delay + + @kernel + def clear_smp_err(self): + """Clear the SMP_ERR flag and enables SMP_ERR validity monitoring. + + Violations of the SYNC_IN sample and hold margins will result in + SMP_ERR being asserted. This then also activates the red LED on + the respective Urukul channel. + + Also modifies CFR2. + """ + self.write32(_AD9910_REG_CFR2, 0x01010020) # clear SMP_ERR + self.cpld.io_update.pulse(1*us) + self.write32(_AD9910_REG_CFR2, 0x01010000) # enable SMP_ERR + self.cpld.io_update.pulse(1*us) + + @kernel + def tune_sync_delay(self, sync_delay_seed): + """Find a stable SYNC_IN delay. + + This method first locates the smallest SYNC_IN validity window at + minimum window size and then increases the window a bit to provide some + slack and stability. + + It starts scanning delays around `sync_delay_seed` (see the + device database arguments and :class:`AD9910`) at maximum validation + window size and decreases the window size until a valid delay is found. + + :param sync_delay_seed: Start value for valid SYNC_IN delay search. + :return: Tuple of optimal delay and window size. + """ + dt = 14 # 1/(f_SYSCLK*75ps) taps per SYSCLK period + max_delay = dt # 14*75ps > 1ns + max_window = dt//4 + 1 # 2*75ps*4 = 600ps high > 1ns/2 + min_window = max(0, max_window - 2) # 2*75ps hold, 2*75ps setup + for window in range(max_window - min_window + 1): + window = max_window - window + for in_delay in range(max_delay): + # alternate search direction around seed_delay + if in_delay & 1: + in_delay = -in_delay + in_delay = sync_delay_seed + (in_delay >> 1) + if in_delay < 0: + in_delay = 0 + elif in_delay > 31: + in_delay = 31 + self.set_sync(in_delay, window) + self.clear_smp_err() + # integrate SMP_ERR statistics for a few hundred cycles + delay(10*us) + err = urukul_sta_smp_err(self.cpld.sta_read()) + err = (err >> (self.chip_select - 4)) & 1 + delay(40*us) # slack + if not err: + window -= min_window # add margin + self.set_sync(in_delay, window) + self.clear_smp_err() + delay(40*us) # slack + return in_delay, window + raise ValueError("no valid window/delay") + + @kernel + def measure_io_update_alignment(self, io_up_delay): + """Use the digital ramp generator to locate the alignment between + IO_UPDATE and SYNC_CLK. + + The ramp generator is set up to a linear frequency ramp + (dFTW/t_SYNC_CLK=1) and started at a RTIO time stamp. + + After scanning the alignment, an IO_UPDATE delay midway between two + edges should be chosen. + + :return: odd/even SYNC_CLK cycle indicator + """ + # set up DRG + # DRG ACC autoclear and LRR on io update + self.write32(_AD9910_REG_CFR1, 0x0000c002) + # DRG -> FTW, DRG enable + self.write32(_AD9910_REG_CFR2, 0x01090000) + # no limits + self.write64(_AD9910_REG_DRAMPL, -1, 0) + # DRCTL=0, dt=1 t_SYNC_CLK + self.write32(_AD9910_REG_DRAMPR, 0x00010000) + # dFTW = 1, (work around negative slope) + self.write64(_AD9910_REG_DRAMPS, -1, 0) + at_mu(now_mu() + 0x10 & ~0xf) # align to RTIO/2 + self.cpld.io_update.pulse_mu(8) + # disable DRG autoclear and LRR on io_update + self.write32(_AD9910_REG_CFR1, 0x00000002) + # stop DRG + self.write64(_AD9910_REG_DRAMPS, 0, 0) + at_mu((now_mu() + 0x10 & ~0xf) + io_up_delay) # delay + self.cpld.io_update.pulse_mu(32 - io_up_delay) # realign + ftw = self.read32(_AD9910_REG_FTW) # read out effective FTW + delay(100*us) # slack + # disable DRG + self.write32(_AD9910_REG_CFR2, 0x01010000) + self.cpld.io_update.pulse_mu(8) + return ftw & 1 + + @kernel + def tune_io_update_delay(self): + """Find a stable IO_UPDATE delay alignment. + + Scan through increasing IO_UPDATE delays until a delay is found that + lets IO_UPDATE be registered in the next SYNC_CLK cycle. Return a + IO_UPDATE delay that is midway between two such SYNC_CLK transitions. + + This method assumes that the IO_UPDATE TTLOut device has one machine + unit resolution (SERDES) and that the ratio between fine RTIO frequency + (RTIO time machine units) and SYNC_CLK is 4. + + :return: Stable IO_UPDATE delay to be passed to the constructor + :class:`AD9910` via the device database. + """ + period = 4 # f_RTIO/f_SYNC = 4 + max_delay = 8 # mu, 1 ns + d0 = self.io_update_delay + t0 = int32(self.measure_io_update_alignment(d0)) + for i in range(max_delay - 1): + t = self.measure_io_update_alignment( + (d0 + i + 1) & (max_delay - 1)) + if t != t0: + return (d0 + i + period//2) & (period - 1) + raise ValueError("no IO_UPDATE-SYNC_CLK alignment edge found") diff --git a/artiq/coredevice/ttl.py b/artiq/coredevice/ttl.py index 2548bab70..f7bf48bb9 100644 --- a/artiq/coredevice/ttl.py +++ b/artiq/coredevice/ttl.py @@ -430,14 +430,15 @@ class TTLClockGen: The time cursor is not modified by any function in this class. :param channel: channel number + :param acc_width: accumulator width in bits """ kernel_invariants = {"core", "channel", "acc_width"} - def __init__(self, dmgr, channel, core_device="core"): + def __init__(self, dmgr, channel, acc_width=24, core_device="core"): self.core = dmgr.get(core_device) self.channel = channel - self.acc_width = numpy.int64(24) + self.acc_width = numpy.int64(acc_width) @portable def frequency_to_ftw(self, frequency): diff --git a/artiq/coredevice/urukul.py b/artiq/coredevice/urukul.py index fe9c1a68c..9f96a92ed 100644 --- a/artiq/coredevice/urukul.py +++ b/artiq/coredevice/urukul.py @@ -1,4 +1,4 @@ -from artiq.language.core import kernel, delay, portable +from artiq.language.core import kernel, delay, portable, at_mu, now_mu from artiq.language.units import us, ms from numpy import int32 @@ -61,7 +61,7 @@ def urukul_cfg(rf_sw, led, profile, io_update, mask_nu, (io_update << CFG_IO_UPDATE) | (mask_nu << CFG_MASK_NU) | ((clk_sel & 0x01) << CFG_CLK_SEL0) | - ((clk_sel & 0x02) << (CFG_CLK_SEL1-1)) | + ((clk_sel & 0x02) << (CFG_CLK_SEL1 - 1)) | (sync_sel << CFG_SYNC_SEL) | (rst << CFG_RST) | (io_rst << CFG_IO_RST)) @@ -109,12 +109,22 @@ class _RegIOUpdate: self.cpld.cfg_write(cfg) +class _DummySync: + def __init__(self, cpld): + self.cpld = cpld + + @kernel + def set_mu(self, ftw): + pass + + class CPLD: """Urukul CPLD SPI router and configuration interface. :param spi_device: SPI bus device name :param io_update_device: IO update RTIO TTLOut channel name :param dds_reset_device: DDS reset RTIO TTLOut channel name + :param sync_device: AD9910 SYNC_IN RTIO TTLClockGen channel name :param refclk: Reference clock (SMA, MMCX or on-board 100 MHz oscillator) frequency in Hz :param clk_sel: Reference clock selection. For hardware revision >= 1.3 @@ -122,20 +132,25 @@ class CPLD: internal MMCX. For hardware revision <= v1.2 valid options are: 0 - either XO or MMCX dependent on component population; 1 SMA. Unsupported clocking options are silently ignored. - :param sync_sel: SYNC clock selection. 0 corresponds to SYNC clock over EEM - from FPGA. 1 corresponds to SYNC clock from DDS0. + :param sync_sel: SYNC_IN selection. 0 corresponds to SYNC_IN over EEM + from FPGA. 1 corresponds to SYNC_IN from DDS0. :param rf_sw: Initial CPLD RF switch register setting (default: 0x0). Knowledge of this state is not transferred between experiments. :param att: Initial attenuator setting shift register (default: 0x00000000). See also: :meth:`set_all_att_mu`. Knowledge of this state is not transferred between experiments. + :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 core_device: Core device name """ kernel_invariants = {"refclk", "bus", "core", "io_update"} def __init__(self, dmgr, spi_device, io_update_device=None, - dds_reset_device=None, sync_sel=0, clk_sel=0, rf_sw=0, - refclk=125e6, att=0x00000000, core_device="core"): + dds_reset_device=None, sync_device=None, + sync_sel=0, clk_sel=0, rf_sw=0, + refclk=125e6, att=0x00000000, sync_div=None, + core_device="core"): self.core = dmgr.get(core_device) self.refclk = refclk @@ -147,11 +162,20 @@ class CPLD: self.io_update = _RegIOUpdate(self) if dds_reset_device is not None: self.dds_reset = dmgr.get(dds_reset_device) + if sync_device is not None: + self.sync = dmgr.get(sync_device) + if sync_div is None: + sync_div = 2 + else: + self.sync = _DummySync(self) + assert sync_div is None + sync_div = 0 self.cfg_reg = urukul_cfg(rf_sw=rf_sw, led=0, profile=0, io_update=0, mask_nu=0, clk_sel=clk_sel, sync_sel=sync_sel, rst=0, io_rst=0) self.att_reg = att + self.sync_div = sync_div @kernel def cfg_write(self, cfg): @@ -207,6 +231,9 @@ class CPLD: 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 @@ -289,3 +316,20 @@ class CPLD: SPIT_ATT_RD, CS_ATT) self.bus.write(self.att_reg) return self.bus.read() + + @kernel + def set_sync_div(self, div): + """Set the SYNC_IN AD9910 pulse generator frequency + and align it to the current RTIO timestamp. + + The SYNC_IN signal is derived from the coarse RTIO clock + and the divider must be a power of two two. + Configure ``sync_sel == 0``. + + :param div: SYNC_IN frequency divider. Must be a power of two. + Minimum division ratio is 2. Maximum division ratio is 16. + """ + ftw_max = 1 << 4 + ftw = ftw_max//div + assert ftw*div == ftw_max + self.sync.set_mu(ftw) diff --git a/artiq/examples/kasli_basic/device_db_hub.py b/artiq/examples/kasli_basic/device_db_hub.py index dc2fc2d59..ea364ecfc 100644 --- a/artiq/examples/kasli_basic/device_db_hub.py +++ b/artiq/examples/kasli_basic/device_db_hub.py @@ -86,13 +86,19 @@ for j in range(3): "type": "local", "module": "artiq.coredevice.spi2", "class": "SPIMaster", - "arguments": {"channel": 27 + 2*j} + "arguments": {"channel": 27 + 3*j} + }, + "ttl_urukul{}_sync".format(j): { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLClockGen", + "arguments": {"channel": 28 + 3*j, "acc_width": 4} }, "ttl_urukul{}_io_update".format(j): { "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", - "arguments": {"channel": 28 + 2*j} + "arguments": {"channel": 29 + 3*j} }, "urukul{}_cpld".format(j): { "type": "local", @@ -100,6 +106,7 @@ for j in range(3): "class": "CPLD", "arguments": { "spi_device": "spi_urukul{}".format(j), + "sync_device": "ttl_urukul{}_sync".format(j), "io_update_device": "ttl_urukul{}_io_update".format(j), "refclk": 100e6, "clk_sel": 0 @@ -126,13 +133,13 @@ device_db.update({ "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", - "arguments": {"channel": 33} + "arguments": {"channel": 36} }, "led1": { "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", - "arguments": {"channel": 34} + "arguments": {"channel": 37} } }) @@ -142,19 +149,19 @@ device_db.update({ "type": "local", "module": "artiq.coredevice.spi2", "class": "SPIMaster", - "arguments": {"channel": 35} + "arguments": {"channel": 38} }, "ttl_zotino0_ldac": { "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", - "arguments": {"channel": 36} + "arguments": {"channel": 39} }, "ttl_zotino0_clr": { "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", - "arguments": {"channel": 37} + "arguments": {"channel": 40} }, "zotino0": { "type": "local", diff --git a/artiq/examples/kasli_basic/device_db_luh.py b/artiq/examples/kasli_basic/device_db_luh.py new file mode 100644 index 000000000..9efdbd190 --- /dev/null +++ b/artiq/examples/kasli_basic/device_db_luh.py @@ -0,0 +1,183 @@ +core_addr = "staging.ber.quartiq.de" + +device_db = { + "core": { + "type": "local", + "module": "artiq.coredevice.core", + "class": "Core", + "arguments": {"host": core_addr, "ref_period": 1e-9} + }, + "core_log": { + "type": "controller", + "host": "::1", + "port": 1068, + "command": "aqctl_corelog -p {port} --bind {bind} " + core_addr + }, + "core_cache": { + "type": "local", + "module": "artiq.coredevice.cache", + "class": "CoreCache" + }, + "core_dma": { + "type": "local", + "module": "artiq.coredevice.dma", + "class": "CoreDMA" + }, + + "i2c_switch0": { + "type": "local", + "module": "artiq.coredevice.i2c", + "class": "PCA9548", + "arguments": {"address": 0xe0} + }, + "i2c_switch1": { + "type": "local", + "module": "artiq.coredevice.i2c", + "class": "PCA9548", + "arguments": {"address": 0xe2} + }, +} + + +device_db.update({ + "ttl" + str(i): { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLInOut" if i < 4 else "TTLOut", + "arguments": {"channel": i}, + } for i in range(24) +}) + + +device_db.update({ + "spi_sampler0_adc": { + "type": "local", + "module": "artiq.coredevice.spi2", + "class": "SPIMaster", + "arguments": {"channel": 24} + }, + "spi_sampler0_pgia": { + "type": "local", + "module": "artiq.coredevice.spi2", + "class": "SPIMaster", + "arguments": {"channel": 25} + }, + "spi_sampler0_cnv": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 26}, + }, + "sampler0": { + "type": "local", + "module": "artiq.coredevice.sampler", + "class": "Sampler", + "arguments": { + "spi_adc_device": "spi_sampler0_adc", + "spi_pgia_device": "spi_sampler0_pgia", + "cnv_device": "spi_sampler0_cnv" + } + } +}) + +for j in range(2): + device_db.update({ + "spi_urukul{}".format(j): { + "type": "local", + "module": "artiq.coredevice.spi2", + "class": "SPIMaster", + "arguments": {"channel": 27 + 3*j} + }, + "ttl_urukul{}_sync".format(j): { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLClockGen", + "arguments": {"channel": 28 + 3*j, "acc_width": 4} + }, + "ttl_urukul{}_io_update".format(j): { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 29 + 3*j} + }, + "urukul{}_cpld".format(j): { + "type": "local", + "module": "artiq.coredevice.urukul", + "class": "CPLD", + "arguments": { + "spi_device": "spi_urukul{}".format(j), + "sync_device": "ttl_urukul{}_sync".format(j), + "io_update_device": "ttl_urukul{}_io_update".format(j), + "refclk": 100e6, + "clk_sel": 0 + } + } + }) + + device_db.update({ + "urukul{}_ch{}".format(j, i): { + "type": "local", + "module": "artiq.coredevice.ad9910", + "class": "AD9910", + "arguments": { + "pll_n": 40, + "chip_select": 4 + i, + "cpld_device": "urukul{}_cpld".format(j) + } + } for i in range(4) + }) + +device_db.update({ + "grabber0": { + "type": "local", + "module": "artiq.coredevice.grabber", + "class": "grabber", + "arguments": {"channel_base": 33} + } +}) + +device_db.update({ + "led0": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 35} + }, + "led1": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 36} + } +}) + +device_db.update({ + "spi_zotino0": { + "type": "local", + "module": "artiq.coredevice.spi2", + "class": "SPIMaster", + "arguments": {"channel": 37} + }, + "ttl_zotino0_ldac": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 38} + }, + "ttl_zotino0_clr": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 39} + }, + "zotino0": { + "type": "local", + "module": "artiq.coredevice.zotino", + "class": "Zotino", + "arguments": { + "spi_device": "spi_zotino0", + "ldac_device": "ttl_zotino0_ldac", + "clr_device": "ttl_zotino0_clr" + } + } +}) diff --git a/artiq/examples/kasli_basic/device_db_opticlock.py b/artiq/examples/kasli_basic/device_db_opticlock.py index 068aba9fa..70c4d22d6 100644 --- a/artiq/examples/kasli_basic/device_db_opticlock.py +++ b/artiq/examples/kasli_basic/device_db_opticlock.py @@ -198,7 +198,7 @@ device_db = { "class": "TTLOut", "arguments": {"channel": 25} }, - "novogorny0" : { + "novogorny0": { "type": "local", "module": "artiq.coredevice.novogorny", "class": "Novogorny", @@ -313,23 +313,94 @@ device_db = { "arguments": {"channel": 33} }, + "spi_urukul1": { + "type": "local", + "module": "artiq.coredevice.spi2", + "class": "SPIMaster", + "arguments": {"channel": 34} + }, + "ttl_urukul1_sync": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLClockGen", + "arguments": {"channel": 35, "acc_width": 4} + }, + "ttl_urukul1_io_update": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 36} + }, + "urukul1_cpld": { + "type": "local", + "module": "artiq.coredevice.urukul", + "class": "CPLD", + "arguments": { + "spi_device": "spi_urukul1", + "sync_device": "ttl_urukul1_sync", + "io_update_device": "ttl_urukul1_io_update", + "refclk": 100e6, + "clk_sel": 1 + } + }, + "urukul1_ch0": { + "type": "local", + "module": "artiq.coredevice.ad9910", + "class": "AD9910", + "arguments": { + "pll_n": 40, + "chip_select": 4, + "cpld_device": "urukul1_cpld" + } + }, + "urukul1_ch1": { + "type": "local", + "module": "artiq.coredevice.ad9910", + "class": "AD9910", + "arguments": { + "pll_n": 40, + "chip_select": 5, + "cpld_device": "urukul1_cpld" + } + }, + "urukul1_ch2": { + "type": "local", + "module": "artiq.coredevice.ad9910", + "class": "AD9910", + "arguments": { + "pll_n": 40, + "chip_select": 6, + "cpld_device": "urukul1_cpld" + } + }, + "urukul1_ch3": { + "type": "local", + "module": "artiq.coredevice.ad9910", + "class": "AD9910", + "arguments": { + "pll_n": 40, + "chip_select": 7, + "cpld_device": "urukul1_cpld" + } + }, + "spi_zotino0": { "type": "local", "module": "artiq.coredevice.spi2", "class": "SPIMaster", - "arguments": {"channel": 36} + "arguments": {"channel": 37} }, "ttl_zotino0_ldac": { "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", - "arguments": {"channel": 37} + "arguments": {"channel": 38} }, "ttl_zotino0_clr": { "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", - "arguments": {"channel": 38} + "arguments": {"channel": 39} }, "zotino0": { "type": "local", diff --git a/artiq/examples/kasli_basic/device_db_ptb2.py b/artiq/examples/kasli_basic/device_db_ptb2.py new file mode 100644 index 000000000..ed6d4722f --- /dev/null +++ b/artiq/examples/kasli_basic/device_db_ptb2.py @@ -0,0 +1,240 @@ +core_addr = "staging.ber.quartiq.de" + +device_db = { + "core": { + "type": "local", + "module": "artiq.coredevice.core", + "class": "Core", + "arguments": {"host": core_addr, "ref_period": 1e-9} + }, + "core_log": { + "type": "controller", + "host": "::1", + "port": 1068, + "command": "aqctl_corelog -p {port} --bind {bind} " + core_addr + }, + "core_cache": { + "type": "local", + "module": "artiq.coredevice.cache", + "class": "CoreCache" + }, + "core_dma": { + "type": "local", + "module": "artiq.coredevice.dma", + "class": "CoreDMA" + }, + + "i2c_switch0": { + "type": "local", + "module": "artiq.coredevice.i2c", + "class": "PCA9548", + "arguments": {"address": 0xe0} + }, + "i2c_switch1": { + "type": "local", + "module": "artiq.coredevice.i2c", + "class": "PCA9548", + "arguments": {"address": 0xe2} + }, +} + + +device_db.update({ + "ttl" + str(i): { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLInOut" if i < 4 else "TTLOut", + "arguments": {"channel": i}, + } for i in range(24) +}) + + +device_db.update({ + "spi_sampler0_adc": { + "type": "local", + "module": "artiq.coredevice.spi2", + "class": "SPIMaster", + "arguments": {"channel": 24} + }, + "spi_sampler0_pgia": { + "type": "local", + "module": "artiq.coredevice.spi2", + "class": "SPIMaster", + "arguments": {"channel": 25} + }, + "spi_sampler0_cnv": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 26}, + }, + "sampler0": { + "type": "local", + "module": "artiq.coredevice.sampler", + "class": "Sampler", + "arguments": { + "spi_adc_device": "spi_sampler0_adc", + "spi_pgia_device": "spi_sampler0_pgia", + "cnv_device": "spi_sampler0_cnv" + } + } +}) + +device_db.update({ + "spi_urukul0": { + "type": "local", + "module": "artiq.coredevice.spi2", + "class": "SPIMaster", + "arguments": {"channel": 27} + }, + "ttl_urukul0_io_update": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 28} + }, + "ttl_urukul0_sw0": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 29} + }, + "ttl_urukul0_sw1": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 30} + }, + "ttl_urukul0_sw2": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 31} + }, + "ttl_urukul0_sw3": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 32} + }, + "urukul0_cpld": { + "type": "local", + "module": "artiq.coredevice.urukul", + "class": "CPLD", + "arguments": { + "spi_device": "spi_urukul0", + "io_update_device": "ttl_urukul0_io_update", + "refclk": 100e6, + "clk_sel": 0 + } + } +}) + +device_db.update({ + "urukul0_ch" + str(i): { + "type": "local", + "module": "artiq.coredevice.ad9912", + "class": "AD9912", + "arguments": { + "pll_n": 10, + "chip_select": 4 + i, + "cpld_device": "urukul0_cpld", + "sw_device": "ttl_urukul0_sw" + str(i) + } + } for i in range(4) +}) + + +device_db.update({ + "spi_urukul1": { + "type": "local", + "module": "artiq.coredevice.spi2", + "class": "SPIMaster", + "arguments": {"channel": 33} + }, + "ttl_urukul1_sync": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLClockGen", + "arguments": {"channel": 34, "acc_width": 4} + }, + "ttl_urukul1_io_update": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 35} + }, + "urukul1_cpld": { + "type": "local", + "module": "artiq.coredevice.urukul", + "class": "CPLD", + "arguments": { + "spi_device": "spi_urukul1", + "sync_device": "ttl_urukul1_sync", + "io_update_device": "ttl_urukul1_io_update", + "refclk": 125e6, + "clk_sel": 0 + } + } +}) + +device_db.update({ + "urukul1_ch" + str(i): { + "type": "local", + "module": "artiq.coredevice.ad9910", + "class": "AD9910", + "arguments": { + "pll_n": 32, + "chip_select": 4 + i, + "cpld_device": "urukul1_cpld" + } + } for i in range(4) +}) + + +device_db.update({ + "led0": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 36} + }, + "led1": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 37} + } +}) + + +device_db.update({ + "spi_zotino0": { + "type": "local", + "module": "artiq.coredevice.spi2", + "class": "SPIMaster", + "arguments": {"channel": 38} + }, + "ttl_zotino0_ldac": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 39} + }, + "ttl_zotino0_clr": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 40} + }, + "zotino0": { + "type": "local", + "module": "artiq.coredevice.zotino", + "class": "Zotino", + "arguments": { + "spi_device": "spi_zotino0", + "ldac_device": "ttl_zotino0_ldac", + "clr_device": "ttl_zotino0_clr" + } + } +}) diff --git a/artiq/examples/kasli_tester/device_db.py b/artiq/examples/kasli_tester/device_db.py index e9779af5d..027fbe2e9 100644 --- a/artiq/examples/kasli_tester/device_db.py +++ b/artiq/examples/kasli_tester/device_db.py @@ -57,36 +57,42 @@ device_db.update( "class": "SPIMaster", "arguments": {"channel": 8} }, - ttl_urukul0_io_update={ + ttl_urukul0_sync={ "type": "local", "module": "artiq.coredevice.ttl", - "class": "TTLOut", - "arguments": {"channel": 9} + "class": "TTLClockGen", + "arguments": {"channel": 9, "acc_width": 4} }, - ttl_urukul0_sw0={ + ttl_urukul0_io_update={ "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", "arguments": {"channel": 10} }, - ttl_urukul0_sw1={ + ttl_urukul0_sw0={ "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", "arguments": {"channel": 11} }, - ttl_urukul0_sw2={ + ttl_urukul0_sw1={ "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", "arguments": {"channel": 12} }, - ttl_urukul0_sw3={ + ttl_urukul0_sw2={ "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", "arguments": {"channel": 13} }, + ttl_urukul0_sw3={ + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 14} + }, urukul0_cpld={ "type": "local", "module": "artiq.coredevice.urukul", @@ -94,6 +100,7 @@ device_db.update( "arguments": { "spi_device": "spi_urukul0", "io_update_device": "ttl_urukul0_io_update", + "sync_device": "ttl_urukul0_sync", "refclk": 125e6, "clk_sel": 0 } @@ -119,19 +126,19 @@ device_db["spi_sampler0_adc"] = { "type": "local", "module": "artiq.coredevice.spi2", "class": "SPIMaster", - "arguments": {"channel": 14} + "arguments": {"channel": 15} } device_db["spi_sampler0_pgia"] = { "type": "local", "module": "artiq.coredevice.spi2", "class": "SPIMaster", - "arguments": {"channel": 15} + "arguments": {"channel": 16} } device_db["spi_sampler0_cnv"] = { "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", - "arguments": {"channel": 16}, + "arguments": {"channel": 17}, } device_db["sampler0"] = { "type": "local", @@ -150,19 +157,19 @@ device_db["spi_zotino0"] = { "type": "local", "module": "artiq.coredevice.spi2", "class": "SPIMaster", - "arguments": {"channel": 17} + "arguments": {"channel": 18} } device_db["ttl_zotino0_ldac"] = { "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", - "arguments": {"channel": 18} + "arguments": {"channel": 19} } device_db["ttl_zotino0_clr"] = { "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", - "arguments": {"channel": 19} + "arguments": {"channel": 20} } device_db["zotino0"] = { "type": "local", @@ -181,7 +188,7 @@ device_db["grabber0"] = { "type": "local", "module": "artiq.coredevice.grabber", "class": "Grabber", - "arguments": {"channel_base": 20} + "arguments": {"channel_base": 21} } @@ -191,13 +198,19 @@ device_db.update( "type": "local", "module": "artiq.coredevice.spi2", "class": "SPIMaster", - "arguments": {"channel": 22} + "arguments": {"channel": 23} + }, + ttl_urukul1_sync={ + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLClockGen", + "arguments": {"channel": 24, "acc_width": 4} }, ttl_urukul1_io_update={ "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", - "arguments": {"channel": 23} + "arguments": {"channel": 25} }, urukul1_cpld={ "type": "local", @@ -206,6 +219,7 @@ device_db.update( "arguments": { "spi_device": "spi_urukul1", "io_update_device": "ttl_urukul1_io_update", + "sync_device": "ttl_urukul1_sync", "refclk": 100e6, "clk_sel": 1 } @@ -231,7 +245,7 @@ for i in range(8): "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", - "arguments": {"channel": 24 + i}, + "arguments": {"channel": 26 + i}, } @@ -241,7 +255,7 @@ for i in range(8): "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", - "arguments": {"channel": 32 + i}, + "arguments": {"channel": 34 + i}, } @@ -250,19 +264,19 @@ device_db["spi_sampler1_adc"] = { "type": "local", "module": "artiq.coredevice.spi2", "class": "SPIMaster", - "arguments": {"channel": 40} + "arguments": {"channel": 42} } device_db["spi_sampler1_pgia"] = { "type": "local", "module": "artiq.coredevice.spi2", "class": "SPIMaster", - "arguments": {"channel": 41} + "arguments": {"channel": 43} } device_db["spi_sampler1_cnv"] = { "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", - "arguments": {"channel": 42}, + "arguments": {"channel": 44}, } device_db["sampler1"] = { "type": "local", @@ -281,19 +295,19 @@ device_db["spi_zotino1"] = { "type": "local", "module": "artiq.coredevice.spi2", "class": "SPIMaster", - "arguments": {"channel": 43} + "arguments": {"channel": 45} } device_db["ttl_zotino1_ldac"] = { "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", - "arguments": {"channel": 44} + "arguments": {"channel": 46} } device_db["ttl_zotino1_clr"] = { "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", - "arguments": {"channel": 45} + "arguments": {"channel": 47} } device_db["zotino1"] = { "type": "local", @@ -312,13 +326,13 @@ device_db.update( "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", - "arguments": {"channel": 46} + "arguments": {"channel": 48} }, led1={ "type": "local", "module": "artiq.coredevice.ttl", "class": "TTLOut", - "arguments": {"channel": 47} + "arguments": {"channel": 49} }, ) @@ -328,4 +342,10 @@ device_db.update( loop_out="ttl4", loop_in="ttl0", + + # Urukul CPLD with sync and io_update, IFC MODE 0b1000 + urukul_cpld="urukul0_cpld", + # Urukul AD9910 with switch TTL, internal 125 MHz MMCX connection + # FIXME: MMCX not connected + # urukul_ad9910="urukul0_ch0", ) diff --git a/artiq/gateware/eem.py b/artiq/gateware/eem.py index 34680a714..96050b748 100644 --- a/artiq/gateware/eem.py +++ b/artiq/gateware/eem.py @@ -75,7 +75,7 @@ class Urukul(_EEM): ), ] ttls = [(6, eem, "io_update"), - (7, eem, "dds_reset")] + (7, eem, "dds_reset_sync_in")] if eem_aux is not None: ttls += [(0, eem_aux, "sync_clk"), (1, eem_aux, "sync_in"), @@ -113,7 +113,7 @@ class Urukul(_EEM): ), ] ttls = [(6, eem0, "io_update"), - (7, eem0, "dds_reset"), + (7, eem0, "dds_reset_sync_in"), (4, eem1, "sw0"), (5, eem1, "sw1"), (6, eem1, "sw2"), @@ -148,7 +148,8 @@ class Urukul(_EEM): return ios @classmethod - def add_std(cls, target, eem, eem_aux, ttl_out_cls, iostandard="LVDS_25"): + def add_std(cls, target, eem, eem_aux, ttl_out_cls, sync_gen_cls=None, + iostandard="LVDS_25"): cls.add_extension(target, eem, eem_aux, iostandard=iostandard) phy = spi2.SPIMaster(target.platform.request("urukul{}_spi_p".format(eem)), @@ -156,8 +157,13 @@ class Urukul(_EEM): target.submodules += phy target.rtio_channels.append(rtio.Channel.from_phy(phy, ififo_depth=4)) - pads = target.platform.request("urukul{}_dds_reset".format(eem)) - target.specials += DifferentialOutput(0, pads.p, pads.n) + pads = target.platform.request("urukul{}_dds_reset_sync_in".format(eem)) + pad = Signal(reset=0) + target.specials += DifferentialOutput(pad, pads.p, pads.n) + if sync_gen_cls is not None: # AD9910 variant and SYNC_IN from EEM + phy = sync_gen_cls(pad, ftw_width=4) + target.submodules += phy + target.rtio_channels.append(rtio.Channel.from_phy(phy)) pads = target.platform.request("urukul{}_io_update".format(eem)) phy = ttl_out_cls(pads.p, pads.n) @@ -170,7 +176,6 @@ class Urukul(_EEM): target.submodules += phy target.rtio_channels.append(rtio.Channel.from_phy(phy)) - class Sampler(_EEM): @staticmethod def io(eem, eem_aux, iostandard="LVDS_25"): @@ -522,7 +527,7 @@ class SUServo(_EEM): target.submodules += phy target.rtio_channels.append(rtio.Channel.from_phy(phy, ififo_depth=4)) - pads = target.platform.request("{}_dds_reset".format(eem_urukul0)) + pads = target.platform.request("{}_dds_reset_sync_in".format(eem_urukul0)) target.specials += DifferentialOutput(0, pads.p, pads.n) for i, signal in enumerate("sw0 sw1 sw2 sw3".split()): diff --git a/artiq/gateware/targets/kasli.py b/artiq/gateware/targets/kasli.py index fc7eaf92a..0475fd9a6 100755 --- a/artiq/gateware/targets/kasli.py +++ b/artiq/gateware/targets/kasli.py @@ -175,7 +175,8 @@ class Opticlock(_StandaloneBase): self.submodules += phy self.rtio_channels.append(rtio.Channel.from_phy(phy)) - eem.Urukul.add_std(self, 6, None, ttl_serdes_7series.Output_8X) + eem.Urukul.add_std(self, 6, None, ttl_serdes_7series.Output_8X, + ttl_simple.ClockGen) eem.Zotino.add_std(self, 7, ttl_serdes_7series.Output_8X) self.config["HAS_RTIO_LOG"] = None @@ -574,6 +575,47 @@ class PTB(_StandaloneBase): self.add_rtio(self.rtio_channels) +class PTB2(_StandaloneBase): + """PTB Kasli variant with Urukul1 SYNC and external reference clock""" + def __init__(self, hw_rev=None, **kwargs): + if hw_rev is None: + hw_rev = "v1.1" + _StandaloneBase.__init__(self, hw_rev=hw_rev, **kwargs) + + self.config["SI5324_AS_SYNTHESIZER"] = None + self.config["SI5324_EXT_REF"] = None + self.config["RTIO_FREQUENCY"] = "125.0" + if hw_rev == "v1.0": + # EEM clock fan-out from Si5324, not MMCX + self.comb += self.platform.request("clk_sel").eq(1) + + self.rtio_channels = [] + eem.DIO.add_std(self, 0, + ttl_serdes_7series.InOut_8X, ttl_serdes_7series.Output_8X) + eem.DIO.add_std(self, 1, + ttl_serdes_7series.Output_8X, ttl_serdes_7series.Output_8X) + eem.DIO.add_std(self, 2, + ttl_serdes_7series.Output_8X, ttl_serdes_7series.Output_8X) + eem.Sampler.add_std(self, 3, None, ttl_serdes_7series.Output_8X) + eem.Urukul.add_std(self, 5, 4, ttl_serdes_7series.Output_8X) + eem.Urukul.add_std(self, 6, None, ttl_serdes_7series.Output_8X, + ttl_simple.ClockGen) + + for i in (1, 2): + sfp_ctl = self.platform.request("sfp_ctl", i) + phy = ttl_simple.Output(sfp_ctl.led) + self.submodules += phy + self.rtio_channels.append(rtio.Channel.from_phy(phy)) + + eem.Zotino.add_std(self, 7, ttl_serdes_7series.Output_8X) + + self.config["HAS_RTIO_LOG"] = None + self.config["RTIO_LOG_CHANNEL"] = len(self.rtio_channels) + self.rtio_channels.append(rtio.LogChannel()) + + self.add_rtio(self.rtio_channels) + + class HUB(_StandaloneBase): """HUB Kasli variant @@ -600,9 +642,12 @@ class HUB(_StandaloneBase): eem.DIO.add_std(self, 2, ttl_serdes_7series.Output_8X, ttl_serdes_7series.Output_8X) eem.Sampler.add_std(self, 3, None, ttl_serdes_7series.Output_8X) - eem.Urukul.add_std(self, 4, None, ttl_serdes_7series.Output_8X) - eem.Urukul.add_std(self, 5, None, ttl_serdes_7series.Output_8X) - eem.Urukul.add_std(self, 6, None, ttl_serdes_7series.Output_8X) + eem.Urukul.add_std(self, 4, None, ttl_serdes_7series.Output_8X, + ttl_simple.ClockGen) + eem.Urukul.add_std(self, 5, None, ttl_serdes_7series.Output_8X, + ttl_simple.ClockGen) + eem.Urukul.add_std(self, 6, None, ttl_serdes_7series.Output_8X, + ttl_simple.ClockGen) for i in (1, 2): sfp_ctl = self.platform.request("sfp_ctl", i) @@ -645,8 +690,10 @@ class LUH(_StandaloneBase): eem.DIO.add_std(self, 2, ttl_serdes_7series.Output_8X, ttl_serdes_7series.Output_8X) eem.Sampler.add_std(self, 3, None, ttl_serdes_7series.Output_8X) - eem.Urukul.add_std(self, 4, None, ttl_serdes_7series.Output_8X) - eem.Urukul.add_std(self, 5, None, ttl_serdes_7series.Output_8X) + eem.Urukul.add_std(self, 4, None, ttl_serdes_7series.Output_8X, + ttl_simple.ClockGen) + eem.Urukul.add_std(self, 5, None, ttl_serdes_7series.Output_8X, + ttl_simple.ClockGen) eem.Grabber.add_std(self, 6) for i in (1, 2): @@ -688,11 +735,13 @@ class Tester(_StandaloneBase): self.grabber_csr_group = [] eem.DIO.add_std(self, 5, ttl_serdes_7series.InOut_8X, ttl_serdes_7series.Output_8X) - eem.Urukul.add_std(self, 0, 1, ttl_serdes_7series.Output_8X) + eem.Urukul.add_std(self, 0, 1, ttl_serdes_7series.Output_8X, + ttl_simple.ClockGen) eem.Sampler.add_std(self, 3, 2, ttl_serdes_7series.Output_8X) eem.Zotino.add_std(self, 4, ttl_serdes_7series.Output_8X) eem.Grabber.add_std(self, 6) - eem.Urukul.add_std(self, 7, None, ttl_serdes_7series.Output_8X) + eem.Urukul.add_std(self, 7, None, ttl_serdes_7series.Output_8X, + ttl_simple.ClockGen) eem.DIO.add_std(self, 8, ttl_serdes_7series.Output_8X, ttl_serdes_7series.Output_8X) eem.DIO.add_std(self, 9, @@ -1079,8 +1128,8 @@ def main(): soc_kasli_args(parser) parser.set_defaults(output_dir="artiq_kasli") variants = {cls.__name__.lower(): cls for cls in [ - Opticlock, SUServo, SYSU, MITLL, MITLL2, USTC, - Tsinghua, Tsinghua2, WIPM, NUDT, PTB, HUB, LUH, + Opticlock, SUServo, PTB, PTB2, HUB, LUH, + SYSU, MITLL, MITLL2, USTC, Tsinghua, Tsinghua2, WIPM, NUDT, VLBAIMaster, VLBAISatellite, Tester, Master, Satellite]} parser.add_argument("-V", "--variant", default="opticlock", help="variant: {} (default: %(default)s)".format( diff --git a/artiq/test/coredevice/test_ad9910.py b/artiq/test/coredevice/test_ad9910.py new file mode 100644 index 000000000..407755df6 --- /dev/null +++ b/artiq/test/coredevice/test_ad9910.py @@ -0,0 +1,180 @@ +from artiq.experiment import * +from artiq.test.hardware_testbench import ExperimentCase +from artiq.coredevice.ad9910 import _AD9910_REG_FTW +from artiq.coredevice.urukul import ( + urukul_sta_smp_err, CFG_CLK_SEL0, CFG_CLK_SEL1) + + +class AD9910Exp(EnvExperiment): + def build(self, runner): + self.setattr_device("core") + self.dev = self.get_device("urukul_ad9910") + self.runner = runner + + def run(self): + getattr(self, self.runner)() + + @kernel + def instantiate(self): + pass + + @kernel + def init(self): + self.core.break_realtime() + self.dev.cpld.init() + self.dev.init() + + @kernel + def init_fail(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 + self.dev.cpld.cfg_write(cfg) + # clk_sel=1, external SMA, should fail PLL lock + self.dev.init() + + @kernel + def set_get(self): + self.core.break_realtime() + self.dev.cpld.init() + self.dev.init() + self.dev.set_att(20*dB) + f = 81.2345*MHz + self.dev.set(frequency=f, phase=.33, amplitude=.89) + self.set_dataset("ftw_set", self.dev.frequency_to_ftw(f)) + self.set_dataset("ftw_get", self.dev.read32(_AD9910_REG_FTW)) + + @kernel + def set_speed(self): + self.core.break_realtime() + self.dev.cpld.init() + self.dev.init() + f = 81.2345*MHz + n = 10 + t0 = self.core.get_rtio_counter_mu() + for i in range(n): + self.dev.set(frequency=f, phase=.33, amplitude=.89) + self.set_dataset("dt", self.core.mu_to_seconds( + self.core.get_rtio_counter_mu() - t0)/n) + + @kernel + def set_speed_mu(self): + self.core.break_realtime() + self.dev.cpld.init() + self.dev.init() + n = 10 + t0 = self.core.get_rtio_counter_mu() + for i in range(n): + self.dev.set_mu(0x12345678, 0x1234, 0x4321) + self.set_dataset("dt", self.core.mu_to_seconds( + self.core.get_rtio_counter_mu() - t0)/n) + + @kernel + def sync_window(self): + self.core.break_realtime() + self.dev.cpld.init() + self.dev.init() + dly, win = self.dev.tune_sync_delay(self.dev.sync_delay_seed) + err = [0] * 32 + self.sync_scan(err, win=win + 1) # tighten window by 2*75ps + self.set_dataset("dly", dly) + self.set_dataset("win", win) + self.set_dataset("err", err) + + @kernel + def sync_scan(self, err, win): + for in_delay in range(len(err)): + self.dev.set_sync(in_delay=in_delay, window=win) + self.dev.clear_smp_err() + delay(10*us) # integrate SMP_ERR statistics + e = urukul_sta_smp_err(self.dev.cpld.sta_read()) + err[in_delay] = (e >> (self.dev.chip_select - 4)) & 1 + delay(50*us) # slack + + @kernel + def io_update_delay(self): + self.core.break_realtime() + self.dev.cpld.init() + self.dev.init() + bins = [0]*8 + self.scan_io_delay(bins) + self.set_dataset("bins", bins) + self.set_dataset("dly", self.dev.io_update_delay) + + @kernel + def scan_io_delay(self, bins): + delay(100*us) + n = 100 + for i in range(n): + for phase in range(len(bins)): + bins[phase] += self.dev.measure_io_update_alignment(phase) + delay(10*ms) + + @kernel + def sw_readback(self): + self.core.break_realtime() + self.dev.cpld.init() + self.dev.init() + self.dev.cfg_sw(0) + self.dev.sw.on() + sw_on = (self.dev.cpld.sta_read() >> (self.dev.chip_select - 4)) & 1 + delay(10*us) + 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)) + + +class AD9910Test(ExperimentCase): + def test_instantiate(self): + self.execute(AD9910Exp, "instantiate") + + def test_init(self): + self.execute(AD9910Exp, "init") + + def test_init_fail(self): + with self.assertRaises(ValueError): + self.execute(AD9910Exp, "init_fail") + + def test_set_get(self): + self.execute(AD9910Exp, "set_get") + ftw_get = self.dataset_mgr.get("ftw_get") + ftw_set = self.dataset_mgr.get("ftw_set") + self.assertEqual(ftw_get, ftw_set) + + def test_set_speed(self): + self.execute(AD9910Exp, "set_speed") + dt = self.dataset_mgr.get("dt") + print(dt) + self.assertLess(dt, 70*us) + + def test_set_speed_mu(self): + self.execute(AD9910Exp, "set_speed_mu") + dt = self.dataset_mgr.get("dt") + print(dt) + self.assertLess(dt, 10*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") + dly = self.dataset_mgr.get("dly") + bins = self.dataset_mgr.get("bins") + print(dly, bins) + n = max(bins) + # test for 4-periodicity (SYNC_CLK) and maximal contrast + for i in range(len(bins)): + self.assertEqual(abs(bins[i] - bins[(i + 4) % 8]), n) + + def test_sw_readback(self): + self.execute(AD9910Exp, "sw_readback") + self.assertEqual(self.dataset_mgr.get("sw"), (1, 0)) diff --git a/artiq/test/coredevice/test_urukul.py b/artiq/test/coredevice/test_urukul.py new file mode 100644 index 000000000..2141303c4 --- /dev/null +++ b/artiq/test/coredevice/test_urukul.py @@ -0,0 +1,149 @@ +from artiq.experiment import * +from artiq.test.hardware_testbench import ExperimentCase +from artiq.coredevice import urukul + + +class UrukulExp(EnvExperiment): + def build(self, runner): + self.setattr_device("core") + self.dev = self.get_device("urukul_cpld") + self.runner = runner + + def run(self): + getattr(self, self.runner)() + + @kernel + def instantiate(self): + pass + + @kernel + def init(self): + self.core.break_realtime() + self.dev.init() + + @kernel + def cfg_write(self): + self.core.break_realtime() + self.dev.init() + self.dev.cfg_write(self.dev.cfg_reg) + + @kernel + def sta_read(self): + self.core.break_realtime() + self.dev.init() + sta = self.dev.sta_read() + self.set_dataset("sta", sta) + + @kernel + def switches(self): + self.core.break_realtime() + self.dev.init() + self.dev.io_rst() + self.dev.cfg_sw(0, 0) + self.dev.cfg_sw(0, 1) + self.dev.cfg_sw(3, 1) + self.dev.cfg_switches(0b1010) + + @kernel + def switch_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_sw(3, i & 1) + self.set_dataset("dt", self.core.mu_to_seconds( + self.core.get_rtio_counter_mu() - t0)/n) + + @kernel + def switches_readback(self): + self.core.reset() # clear switch TTLs + self.dev.init() + sw_set = 0b1010 + self.dev.cfg_switches(sw_set) + sta_get = self.dev.sta_read() + self.set_dataset("sw_set", sw_set) + self.set_dataset("sta_get", sta_get) + + @kernel + def att(self): + self.core.break_realtime() + self.dev.init() + att_set = 0x12345678 + self.dev.set_all_att_mu(att_set) + att_get = self.dev.get_att_mu() + self.set_dataset("att_set", att_set) + self.set_dataset("att_get", att_get) + + @kernel + def att_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.set_att(3, 30*dB) + self.set_dataset("dt", self.core.mu_to_seconds( + self.core.get_rtio_counter_mu() - t0)/n) + + @kernel + def io_update(self): + self.core.break_realtime() + self.dev.init() + self.dev.io_update.pulse_mu(8) + + @kernel + def sync(self): + self.core.break_realtime() + self.dev.init() + self.dev.set_sync_div(2) + + +class UrukulTest(ExperimentCase): + def test_instantiate(self): + self.execute(UrukulExp, "instantiate") + + def test_init(self): + self.execute(UrukulExp, "init") + + def test_cfg_write(self): + self.execute(UrukulExp, "cfg_write") + + 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_switches(self): + self.execute(UrukulExp, "switches") + + def test_switch_speed(self): + self.execute(UrukulExp, "switch_speed") + dt = self.dataset_mgr.get("dt") + print(dt) + self.assertLess(dt, 5*us) + + def test_switches_readback(self): + self.execute(UrukulExp, "switches_readback") + sw_get = urukul.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(self): + self.execute(UrukulExp, "att") + att_set = self.dataset_mgr.get("att_set") + att_get = self.dataset_mgr.get("att_get") + self.assertEqual(att_set, att_get) + + def test_att_speed(self): + self.execute(UrukulExp, "att_speed") + dt = self.dataset_mgr.get("dt") + print(dt) + self.assertLess(dt, 5*us) + + def test_io_update(self): + self.execute(UrukulExp, "io_update") + + def test_sync(self): + self.execute(UrukulExp, "sync")