diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index 19a97a206..69c725337 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -3,48 +3,66 @@ Release notes ============= +Unreleased +---------- + +Highlights: + +* Implemented Phaser-servo. This requires recent gateware on Phaser. + + ARTIQ-7 ------- Highlights: * New hardware support: - - Kasli-SoC, a new EEM carrier based on a Zynq SoC, enabling much faster kernel execution. + - Kasli-SoC, a new EEM carrier based on a Zynq SoC, enabling much faster kernel execution + (see: https://arxiv.org/abs/2111.15290). + - DRTIO support on Zynq-based devices (Kasli-SoC and ZC706). + - DRTIO support on KC705. - HVAMP_8CH 8 channel HV amplifier for Fastino / Zotinos - Almazny mezzanine board for Mirny -* TTL device output can be now configured to work as a clock generator. + - Phaser: improved documentation, exposed the DAC coarse mixer and ``sif_sync``, exposed upconverter calibration + and enabling/disabling of upconverter LO & RF outputs, added helpers to align Phaser updates to the + RTIO timeline (``get_next_frame_mu()`` + - Urukul: ``get()``, ``get_mu()``, ``get_att()``, and ``get_att_mu()`` functions added for AD9910 and AD9912. * Softcore targets now use the RISC-V architecture (VexRiscv) instead of OR1K (mor1kx). * Gateware FPU is supported on KC705 and Kasli 2.0. * Faster compilation for large arrays/lists. -* Phaser: - - Improved documentation - - Expose the DAC coarse mixer and ``sif_sync`` - - Exposes upconverter calibration and enabling/disabling of upconverter LO & RF outputs. - - Add helpers to align Phaser updates to the RTIO timeline (``get_next_frame_mu()``) -* Core device moninj is now proxied via the ``aqctl_moninj_proxy`` controller. +* Faster exception handling. +* Several exception handling bugs fixed. +* Support for a simpler shared library system with faster calls into the runtime. This is only used by the NAC3 + compiler (nac3ld) and improves RTIO output performance (test_pulse_rate) by 9-10%. +* Moninj improvements: + - Urukul monitoring and frequency setting (through dashboard) is now supported. + - Core device moninj is now proxied via the ``aqctl_moninj_proxy`` controller. * The configuration entry ``rtio_clock`` supports multiple clocking settings, deprecating the usage of compile-time options. -* Packaging via Nix Flakes. -* Firmware and gateware can now be built on-demand on the M-Labs server using ``afws_client`` - (subscribers only). +* Added support for 100MHz RTIO clock in DRTIO. +* Previously detected RTIO async errors are reported to the host after each kernel terminates and a + warning is logged. The warning is additional to the one already printed in the core device log + immediately upon detection of the error. * Extended Kasli gateware JSON description with configuration for SPI over DIO. -* ``get()``, ``get_mu()``, ``get_att()``, and ``get_att_mu()`` functions added for AD9910 and AD9912. +* TTL outputs can be now configured to work as a clock generator from the JSON. * On Kasli, the number of FIFO lanes in the scalable events dispatcher (SED) can now be configured in - the JSON hardware description file. + the JSON. * ``artiq_ddb_template`` generates edge-counter keys that start with the key of the corresponding TTL device (e.g. ``ttl_0_counter`` for the edge counter on TTL device ``ttl_0``). * ``artiq_master`` now has an ``--experiment-subdir`` option to scan only a subdirectory of the repository when building the list of experiments. -* Added support for 100MHz RTIO clock in DRTIO. -* Previously detected RTIO async errors are reported to the host after each kernel terminates and a - warning is logged. The warning is additional to the one already printed in the core device log upon - detection of the error. +* Experiments can now be submitted by-content. +* The master can now optionally log all experiments submitted into a CSV file. * Removed worker DB warning for writing a dataset that is also in the archive. -* ``PCA9548`` I2C switch class renamed to ``I2CSwitch``, to accomodate support for PCA9547, and - possibly other switches in future. Readback has been removed, and now only one channel per - switch is supported. -* The "ip" config option can now be set to "use_dhcp" in order to use DHCP to obtain an IP address. - DHCP will also be used if no "ip" config option is set. +* Experiments can now call ``scheduler.check_termination()`` to test if the user + has requested graceful termination. +* ARTIQ command-line programs and controllers now exit cleanly on Ctrl-C. +* ``artiq_coremgmt reboot`` now reloads gateware as well, providing a more thorough and reliable + device reset (7-series FPGAs only). +* Firmware and gateware can now be built on-demand on the M-Labs server using ``afws_client`` + (subscribers only). Self-compilation remains possible. +* Easier-to-use packaging via Nix Flakes. +* Python 3.10 support (experimental). Breaking changes: @@ -59,6 +77,10 @@ Breaking changes: * Mirny: Added extra delays in ``ADF5356.sync()``. This avoids the need of an extra delay before calling `ADF5356.init()`. * The deprecated ``set_dataset(..., save=...)`` is no longer supported. +* The ``PCA9548`` I2C switch class was renamed to ``I2CSwitch``, to accomodate support for PCA9547, + and possibly other switches in future. Readback has been removed, and now only one channel per + switch is supported. + ARTIQ-6 ------- diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index dd22b6449..1a61428c2 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -41,6 +41,14 @@ PHASER_ADDR_DUC1_P = 0x26 PHASER_ADDR_DAC1_DATA = 0x28 PHASER_ADDR_DAC1_TEST = 0x2c +# servo registers +PHASER_ADDR_SERVO_CFG0 = 0x30 +PHASER_ADDR_SERVO_CFG1 = 0x31 + +# 0x32 - 0x71 servo coefficients + offset data +PHASER_ADDR_SERVO_DATA_BASE = 0x32 + + PHASER_SEL_DAC = 1 << 0 PHASER_SEL_TRF0 = 1 << 1 PHASER_SEL_TRF1 = 1 << 2 @@ -59,6 +67,11 @@ PHASER_DAC_SEL_TEST = 1 PHASER_HW_REV_VARIANT = 1 << 4 +SERVO_COEFF_WIDTH = 16 +SERVO_DATA_WIDTH = 16 +SERVO_COEFF_SHIFT = 14 +SERVO_T_CYCLE = (32+12+192+24+4)*ns # Must match gateware ADC parameters + @nac3 class Phaser: @@ -114,6 +127,31 @@ class Phaser: configured through a shared SPI bus that is accessed and controlled via FPGA registers. + Each phaser output channel features a servo to control the RF output amplitude + using feedback from an ADC. The servo consists of a first order IIR (infinite + impulse response) filter fed by the ADC and a multiplier that scales the I + and Q datastreams from the DUC by the IIR output. The IIR state is updated at + the 3.788 MHz ADC sampling rate. + + Each channel IIR features 4 profiles, each consisting of the [b0, b1, a1] filter + coefficients as well as an output offset. The coefficients and offset can be + set for each profile individually and the profiles each have their own ``y0``, + ``y1`` output registers (the ``x0``, ``x1`` inputs are shared). To avoid + transient effects, care should be taken to not update the coefficents in the + currently selected profile. + + The servo can be en- or disabled for each channel. When disabled, the servo + output multiplier is simply bypassed and the datastream reaches the DAC unscaled. + + The IIR output can be put on hold for each channel. In hold mode, the filter + still ingests samples and updates its input ``x0`` and ``x1`` registers, but + does not update the ``y0``, ``y1`` output registers. + + After power-up the servo is disabled, in profile 0, with coefficients [0, 0, 0] + and hold is enabled. If older gateware without ther servo is loaded onto the + Phaser FPGA, the device simply behaves as if the servo is disabled and none of + the servo functions have any effect. + .. note:: Various register settings of the DAC and the quadrature upconverters are available to be modified through the `dac`, `trf0`, `trf1` dictionaries. These can be set through the device database @@ -322,6 +360,8 @@ class Phaser: self.core.delay(.1*ms) channel.set_att_mu(0x00) # minimum attenuation + channel.set_servo(profile=0, enable=False, hold=True) + # test oscillators and DUC for i in range(len(channel.oscillator)): oscillator = channel.oscillator[i] @@ -394,6 +434,12 @@ class Phaser: response = rtio_input_data(self.channel_base) return response >> self.miso_delay + @kernel + def write16(self, addr: int32, data: int32): + """Write 16 bit to a sequence of FPGA registers.""" + self.write8(addr, data >> 8) + self.write8(addr + 1, data) + @kernel def write32(self, addr: int32, data: int32): """Write 32 bit to a sequence of FPGA registers.""" @@ -1059,6 +1105,133 @@ class PhaserChannel: data = data ^ ((1 << 12) | (1 << 13)) self.trf_write(data) + @kernel + def set_servo(self, profile: int32 = 0, enable: bool = False, hold: bool = False): + """Set the servo configuration. + + :param enable: True to enable servo, False to disable servo (default). If disabled, + the servo is bypassed and hold is enforced since the control loop is broken. + :param hold: True to hold the servo IIR filter output constant, False for normal operation. + :param profile: Profile index to select for channel. (0 to 3) + """ + if (profile < 0) or (profile > 3): + raise ValueError("invalid profile index") + addr = PHASER_ADDR_SERVO_CFG0 + self.index + # enforce hold if the servo is disabled + data = (profile << 2) | ((int32(hold) | int32(not enable)) << 1) | int32(enable) + self.phaser.write8(addr, data) + + @kernel + def set_iir_mu(self, profile: int32, b0: int32, b1: int32, a1: int32, offset: int32): + """Load a servo profile consiting of the three filter coefficients and an output offset. + + Avoid setting the IIR parameters of the currently active profile. + + The recurrence relation is (all data signed and MSB aligned): + + .. math:: + a_0 y_n = a_1 y_{n - 1} + b_0 x_n + b_1 x_{n - 1} + o + + Where: + + * :math:`y_n` and :math:`y_{n-1}` are the current and previous + filter outputs, clipped to :math:`[0, 1[`. + * :math:`x_n` and :math:`x_{n-1}` are the current and previous + filter inputs in :math:`[-1, 1[`. + * :math:`o` is the offset + * :math:`a_0` is the normalization factor :math:`2^{14}` + * :math:`a_1` is the feedback gain + * :math:`b_0` and :math:`b_1` are the feedforward gains for the two + delays + + .. seealso:: :meth:`set_iir` + + :param profile: Profile to set (0 to 3) + :param b0: b0 filter coefficient (16 bit signed) + :param b1: b1 filter coefficient (16 bit signed) + :param a1: a1 filter coefficient (16 bit signed) + :param offset: Output offset (16 bit signed) + """ + if (profile < 0) or (profile > 3): + raise ValueError("invalid profile index") + # 32 byte-sized data registers per channel and 8 (2 bytes * (3 coefficients + 1 offset)) registers per profile + addr = PHASER_ADDR_SERVO_DATA_BASE + (8 * profile) + (self.index * 32) + for data in [b0, b1, a1, offset]: + self.phaser.write16(addr, data) + addr += 2 + + @kernel + def set_iir(self, profile: int32, kp: float, ki: float = 0., g: float = 0., x_offset: float = 0., y_offset: float = 0.): + """Set servo profile IIR coefficients. + + Avoid setting the IIR parameters of the currently active profile. + + Gains are given in units of output full per scale per input full scale. + + .. note:: Due to inherent constraints of the fixed point datatypes and IIR + filters, the ``x_offset`` (setpoint) resolution depends on the selected gains. + Low ``ki`` gains will lead to a low ``x_offset`` resolution. + + The transfer function is (up to time discretization and + coefficient quantization errors): + + .. math:: + H(s) = k_p + \\frac{k_i}{s + \\frac{k_i}{g}} + + Where: + * :math:`s = \\sigma + i\\omega` is the complex frequency + * :math:`k_p` is the proportional gain + * :math:`k_i` is the integrator gain + * :math:`g` is the integrator gain limit + + :param profile: Profile number (0-3) + :param kp: Proportional gain. This is usually negative (closed + loop, positive ADC voltage, positive setpoint). When 0, this + implements a pure I controller. + :param ki: Integrator gain (rad/s). Equivalent to the gain at 1 Hz. + When 0 (the default) this implements a pure P controller. + Same sign as ``kp``. + :param g: Integrator gain limit (1). When 0 (the default) the + integrator gain limit is infinite. Same sign as ``ki``. + :param x_offset: IIR input offset. Used as the negative + setpoint when stabilizing to a desired input setpoint. Will + be converted to an equivalent output offset and added to y_offset. + :param y_offset: IIR output offset. + """ + NORM = 1 << SERVO_COEFF_SHIFT + COEFF_MAX = 1 << SERVO_COEFF_WIDTH - 1 + DATA_MAX = 1 << SERVO_DATA_WIDTH - 1 + + kp *= float(NORM) + if ki == 0.: + # pure P + a1 = 0 + b1 = 0 + b0 = round(kp) + else: + # I or PI + ki *= float(NORM)*SERVO_T_CYCLE/2. + if g == 0.: + c = 1. + a1 = NORM + else: + c = 1./(1. + ki/(g*float(NORM))) + a1 = round((2.*c - 1.)*float(NORM)) + b0 = round(kp + ki*c) + b1 = round(kp + (ki - 2.*kp)*c) + if b1 == -b0: + raise ValueError("low integrator gain and/or gain limit") + + if (b0 >= COEFF_MAX or b0 < -COEFF_MAX or + b1 >= COEFF_MAX or b1 < -COEFF_MAX): + raise ValueError("high gains") + + forward_gain = (b0 + b1) * (1 << SERVO_DATA_WIDTH - 1 - SERVO_COEFF_SHIFT) + effective_offset = round(float(DATA_MAX) * y_offset + float(forward_gain) * x_offset) + + self.set_iir_mu(profile, b0, b1, a1, effective_offset) + + @nac3 class PhaserOscillator: diff --git a/artiq/dashboard/moninj.py b/artiq/dashboard/moninj.py index df5854259..1a58ad90d 100644 --- a/artiq/dashboard/moninj.py +++ b/artiq/dashboard/moninj.py @@ -8,6 +8,8 @@ from PyQt5 import QtCore, QtWidgets, QtGui from sipyco.sync_struct import Subscriber from artiq.coredevice.comm_moninj import * +from artiq.coredevice.ad9910 import _AD9910_REG_PROFILE0, _AD9910_REG_PROFILE7, _AD9910_REG_FTW +from artiq.coredevice.ad9912_reg import AD9912_POW1 from artiq.gui.tools import LayoutWidget from artiq.gui.flowlayout import FlowLayout @@ -179,14 +181,45 @@ class _SimpleDisplayWidget(QtWidgets.QFrame): raise NotImplementedError +class _DDSModel: + def __init__(self, dds_type, ref_clk, cpld=None, pll=1, clk_div=0): + self.cpld = cpld + self.cur_frequency = 0 + self.cur_reg = 0 + self.dds_type = dds_type + self.is_urukul = dds_type in ["AD9910", "AD9912"] + + if dds_type == "AD9914": + self.ftw_per_hz = 2**32 / ref_clk + else: + if dds_type == "AD9910": + max_freq = 1 << 32 + clk_mult = [4, 1, 2, 4] + elif dds_type == "AD9912": # AD9912 + max_freq = 1 << 48 + clk_mult = [1, 1, 2, 4] + else: + raise NotImplementedError + sysclk = ref_clk / clk_mult[clk_div] * pll + self.ftw_per_hz = 1 / sysclk * max_freq + + def monitor_update(self, probe, value): + if self.dds_type == "AD9912": + value = value << 16 + self.cur_frequency = self._ftw_to_freq(value) + + def _ftw_to_freq(self, ftw): + return ftw / self.ftw_per_hz + + class _DDSWidget(QtWidgets.QFrame): - def __init__(self, dm, title, bus_channel=0, channel=0, cpld=None): + def __init__(self, dm, title, bus_channel=0, channel=0, dds_model=None): self.dm = dm self.bus_channel = bus_channel self.channel = channel self.dds_name = title - self.cpld = cpld self.cur_frequency = 0 + self.dds_model = dds_model QtWidgets.QFrame.__init__(self) @@ -249,7 +282,7 @@ class _DDSWidget(QtWidgets.QFrame): set_grid.addWidget(set_btn, 0, 1, 1, 1) # for urukuls also allow switching off RF - if self.cpld: + if self.dds_model.is_urukul: off_btn = QtWidgets.QToolButton() off_btn.setText("Off") off_btn.setToolTip("Switch off the output") @@ -276,7 +309,7 @@ class _DDSWidget(QtWidgets.QFrame): set_btn.clicked.connect(self.set_clicked) apply.clicked.connect(self.apply_changes) - if self.cpld: + if self.dds_model.is_urukul: off_btn.clicked.connect(self.off_clicked) self.value_edit.returnPressed.connect(lambda: self.apply_changes(None)) self.value_edit.escapePressedConnect(self.cancel_changes) @@ -293,19 +326,20 @@ class _DDSWidget(QtWidgets.QFrame): self.value_edit.selectAll() def off_clicked(self, set): - self.dm.dds_channel_toggle(self.dds_name, self.cpld, sw=False) + self.dm.dds_channel_toggle(self.dds_name, self.dds_model, sw=False) def apply_changes(self, apply): self.data_stack.setCurrentIndex(0) self.button_stack.setCurrentIndex(0) frequency = float(self.value_edit.text())*1e6 - self.dm.dds_set_frequency(self.dds_name, self.cpld, frequency) + self.dm.dds_set_frequency(self.dds_name, self.dds_model, frequency) def cancel_changes(self, cancel): self.data_stack.setCurrentIndex(0) self.button_stack.setCurrentIndex(0) def refresh_display(self): + self.cur_frequency = self.dds_model.cur_frequency self.value_label.setText("{:.7f}" .format(self.cur_frequency/1e6)) self.value_edit.setText("{:.7f}" @@ -356,7 +390,8 @@ def setup_from_ddb(ddb): bus_channel = v["arguments"]["bus_channel"] channel = v["arguments"]["channel"] dds_sysclk = v["arguments"]["sysclk"] - widget = _WidgetDesc(k, comment, _DDSWidget, (k, bus_channel, channel)) + model = _DDSModel(v["class"], dds_sysclk) + widget = _WidgetDesc(k, comment, _DDSWidget, (k, bus_channel, channel, model)) description.add(widget) elif (v["module"] == "artiq.coredevice.ad9910" and v["class"] == "AD9910") or \ @@ -368,7 +403,11 @@ def setup_from_ddb(ddb): dds_cpld = v["arguments"]["cpld_device"] spi_dev = ddb[dds_cpld]["arguments"]["spi_device"] bus_channel = ddb[spi_dev]["arguments"]["channel"] - widget = _WidgetDesc(k, comment, _DDSWidget, (k, bus_channel, channel, dds_cpld)) + pll = v["arguments"]["pll_n"] + refclk = ddb[dds_cpld]["arguments"]["refclk"] + clk_div = v["arguments"].get("clk_div", 0) + model = _DDSModel( v["class"], refclk, dds_cpld, pll, clk_div) + widget = _WidgetDesc(k, comment, _DDSWidget, (k, bus_channel, channel, model)) description.add(widget) elif ( (v["module"] == "artiq.coredevice.ad53xx" and v["class"] == "AD53xx") or (v["module"] == "artiq.coredevice.zotino" and v["class"] == "Zotino")): @@ -385,7 +424,7 @@ def setup_from_ddb(ddb): mi_port = v.get("port_proxy", 1383) except KeyError: pass - return mi_addr, mi_port, dds_sysclk, description + return mi_addr, mi_port, description class _DeviceManager: @@ -415,15 +454,13 @@ class _DeviceManager: return ddb def notify(self, mod): - mi_addr, mi_port, dds_sysclk, description = setup_from_ddb(self.ddb) + mi_addr, mi_port, description = setup_from_ddb(self.ddb) if (mi_addr, mi_port) != (self.mi_addr, self.mi_port): self.mi_addr = mi_addr self.mi_port = mi_port self.reconnect_mi.set() - self.dds_sysclk = dds_sysclk - for to_remove in self.description - description: widget = self.widgets_by_uid[to_remove.uid] del self.widgets_by_uid[to_remove.uid] @@ -497,7 +534,7 @@ class _DeviceManager: "log_level": logging.WARNING, "content": content, "class_name": class_name, - "arguments": [] + "arguments": {} } scheduling = { "pipeline_name": "main", @@ -512,24 +549,25 @@ class _DeviceManager: scheduling["flush"]) logger.info("Submitted '%s', RID is %d", title, rid) - def dds_set_frequency(self, dds_channel, dds_cpld, freq): + def dds_set_frequency(self, dds_channel, dds_model, freq): # create kernel and fill it in and send-by-content - if dds_cpld: + if dds_model.is_urukul: # urukuls need CPLD init and switch to on # keep previous config if it was set already cpld_dev = """self.setattr_device("core_cache") - self.setattr_device("{}")""".format(dds_cpld) + self.setattr_device("{}")""".format(dds_model.cpld) cpld_init = """cfg = self.core_cache.get("_{cpld}_cfg") if len(cfg) > 0: self.{cpld}.cfg_reg = cfg[0] else: + delay(15*ms) self.{cpld}.init() self.core_cache.put("_{cpld}_cfg", [self.{cpld}.cfg_reg]) cfg = self.core_cache.get("_{cpld}_cfg") - """.format(cpld=dds_cpld) + """.format(cpld=dds_model.cpld) cfg_sw = """self.{}.cfg_sw(True) cfg[0] = self.{}.cfg_reg - """.format(dds_channel, dds_cpld) + """.format(dds_channel, dds_model.cpld) else: cpld_dev = "" cpld_init = "" @@ -546,8 +584,8 @@ class _DeviceManager: @kernel def run(self): self.core.break_realtime() - delay(2*ms) {cpld_init} + delay(5*ms) self.{dds_channel}.init() self.{dds_channel}.set({freq}) {cfg_sw} @@ -560,7 +598,7 @@ class _DeviceManager: "SetDDS", "Set DDS {} {}MHz".format(dds_channel, freq/1e6))) - def dds_channel_toggle(self, dds_channel, dds_cpld, sw=True): + def dds_channel_toggle(self, dds_channel, dds_model, sw=True): # urukul only toggle_exp = textwrap.dedent(""" from artiq.experiment import * @@ -575,18 +613,19 @@ class _DeviceManager: @kernel def run(self): self.core.break_realtime() - delay(2*ms) cfg = self.core_cache.get("_{cpld}_cfg") if len(cfg) > 0: self.{cpld}.cfg_reg = cfg[0] else: + delay(15*ms) self.{cpld}.init() self.core_cache.put("_{cpld}_cfg", [self.{cpld}.cfg_reg]) cfg = self.core_cache.get("_{cpld}_cfg") + delay(5*ms) self.{ch}.init() self.{ch}.cfg_sw({sw}) cfg[0] = self.{cpld}.cfg_reg - """.format(ch=dds_channel, cpld=dds_cpld, sw=sw)) + """.format(ch=dds_channel, cpld=dds_model.cpld, sw=sw)) asyncio.ensure_future( self._submit_by_content( toggle_exp, @@ -619,11 +658,11 @@ class _DeviceManager: elif probe == TTLProbe.oe.value: widget.cur_oe = bool(value) widget.refresh_display() - if (channel, probe) in self.dds_widgets: + elif (channel, probe) in self.dds_widgets: widget = self.dds_widgets[(channel, probe)] - widget.cur_frequency = value*self.dds_sysclk/2**32 + widget.dds_model.monitor_update(probe, value) widget.refresh_display() - if (channel, probe) in self.dac_widgets: + elif (channel, probe) in self.dac_widgets: widget = self.dac_widgets[(channel, probe)] widget.cur_value = value widget.refresh_display() @@ -656,11 +695,11 @@ class _DeviceManager: logger.info("cancelled connection to moninj") break except: - logger.error("failed to connect to moninj", exc_info=True) + logger.error("failed to connect to moninj. Is aqctl_moninj_proxy running?", exc_info=True) await asyncio.sleep(10.) self.reconnect_mi.set() else: - logger.info("ARTIQ dashboard connected to moninj proxy (%s)", + logger.info("ARTIQ dashboard connected to moninj (%s)", self.mi_addr) self.mi_connection = new_mi_connection for ttl_channel in self.ttl_widgets.keys(): diff --git a/artiq/frontend/aqctl_moninj_proxy.py b/artiq/frontend/aqctl_moninj_proxy.py index 8ce6581a9..fcf9e259d 100755 --- a/artiq/frontend/aqctl_moninj_proxy.py +++ b/artiq/frontend/aqctl_moninj_proxy.py @@ -116,6 +116,9 @@ class MonitorMux: else: raise ValueError + def disconnect_cb(self): + self.listeners.clear() + class ProxyConnection: def __init__(self, monitor_mux, reader, writer): @@ -203,7 +206,9 @@ def main(): signal_handler.setup() try: monitor_mux = MonitorMux() - comm_moninj = CommMonInj(monitor_mux.monitor_cb, monitor_mux.injection_status_cb) + comm_moninj = CommMonInj(monitor_mux.monitor_cb, + monitor_mux.injection_status_cb, + monitor_mux.disconnect_cb) monitor_mux.comm_moninj = comm_moninj loop.run_until_complete(comm_moninj.connect(args.core_addr)) try: diff --git a/artiq/frontend/artiq_flash.py b/artiq/frontend/artiq_flash.py index 4c8d30ca3..136b58c71 100755 --- a/artiq/frontend/artiq_flash.py +++ b/artiq/frontend/artiq_flash.py @@ -114,7 +114,12 @@ class Programmer: "telnet_port disabled" ] + preinit_script self._loaded = defaultdict(lambda: None) - self._script = ["init"] + self._script = [ + "set error_msg \"Trying to use configured scan chain anyway\"", + "if {[string first $error_msg [capture \"init\"]] != -1} {", + "puts \"Found error and exiting\"", + "exit}" + ] def _transfer_script(self, script): if isinstance(self._client, LocalClient): diff --git a/artiq/frontend/artiq_master.py b/artiq/frontend/artiq_master.py index 99c2fc9f1..da57e6b3d 100755 --- a/artiq/frontend/artiq_master.py +++ b/artiq/frontend/artiq_master.py @@ -128,6 +128,7 @@ def main(): "scheduler_request_termination": scheduler.request_termination, "scheduler_get_status": scheduler.get_status, "scheduler_check_pause": scheduler.check_pause, + "scheduler_check_termination": scheduler.check_termination, "ccb_issue": ccb_issue, }) experiment_db.scan_repository_async() diff --git a/artiq/gateware/drtio/transceiver/gtx_7series.py b/artiq/gateware/drtio/transceiver/gtx_7series.py index 28b9536db..3d96971ca 100644 --- a/artiq/gateware/drtio/transceiver/gtx_7series.py +++ b/artiq/gateware/drtio/transceiver/gtx_7series.py @@ -295,7 +295,10 @@ class GTX(Module, TransceiverInterface): i_CEB=stable_clkin_n, i_I=clock_pads.p, i_IB=clock_pads.n, - o_O=refclk + o_O=refclk, + p_CLKCM_CFG="0b1", + p_CLKRCV_TRST="0b1", + p_CLKSWING_CFG="0b11" ) rtio_tx_clk = Signal() diff --git a/artiq/gateware/eem.py b/artiq/gateware/eem.py index ae47154c4..88e28d959 100644 --- a/artiq/gateware/eem.py +++ b/artiq/gateware/eem.py @@ -3,7 +3,7 @@ from migen.build.generic_platform import * from migen.genlib.io import DifferentialOutput from artiq.gateware import rtio -from artiq.gateware.rtio.phy import spi2, ad53xx_monitor, grabber +from artiq.gateware.rtio.phy import spi2, ad53xx_monitor, dds, grabber from artiq.gateware.suservo import servo, pads as servo_pads from artiq.gateware.rtio.phy import servo as rtservo, fastino, phaser @@ -222,13 +222,13 @@ class Urukul(_EEM): return ios @classmethod - def add_std(cls, target, eem, eem_aux, ttl_out_cls, sync_gen_cls=None, iostandard=default_iostandard): + def add_std(cls, target, eem, eem_aux, ttl_out_cls, dds_type, sync_gen_cls=None, iostandard=default_iostandard): cls.add_extension(target, eem, eem_aux, iostandard=iostandard) - phy = spi2.SPIMaster(target.platform.request("urukul{}_spi_p".format(eem)), + spi_phy = spi2.SPIMaster(target.platform.request("urukul{}_spi_p".format(eem)), target.platform.request("urukul{}_spi_n".format(eem))) - target.submodules += phy - target.rtio_channels.append(rtio.Channel.from_phy(phy, ififo_depth=4)) + target.submodules += spi_phy + target.rtio_channels.append(rtio.Channel.from_phy(spi_phy, ififo_depth=4)) pads = target.platform.request("urukul{}_dds_reset_sync_in".format(eem)) if sync_gen_cls is not None: # AD9910 variant and SYNC_IN from EEM @@ -237,9 +237,14 @@ class Urukul(_EEM): 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) - target.submodules += phy - target.rtio_channels.append(rtio.Channel.from_phy(phy)) + io_upd_phy = ttl_out_cls(pads.p, pads.n) + 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) + target.submodules += dds_monitor + spi_phy.probes.extend(dds_monitor.probes) + if eem_aux is not None: for signal in "sw0 sw1 sw2 sw3".split(): pads = target.platform.request("urukul{}_{}".format(eem, signal)) @@ -247,6 +252,7 @@ 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): diff --git a/artiq/gateware/eem_7series.py b/artiq/gateware/eem_7series.py index 141923c8d..a6661f881 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, - sync_gen_cls, **kwargs) + peripheral["dds"], sync_gen_cls, **kwargs) def peripheral_sampler(module, peripheral, **kwargs): diff --git a/artiq/gateware/rtio/phy/dds.py b/artiq/gateware/rtio/phy/dds.py index c542937fa..ca7494c20 100644 --- a/artiq/gateware/rtio/phy/dds.py +++ b/artiq/gateware/rtio/phy/dds.py @@ -3,6 +3,11 @@ from migen import * from artiq.gateware import ad9_dds 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.ad9912_reg import AD9912_POW1 +from artiq.coredevice.ad9910 import _AD9910_REG_PROFILE0, _AD9910_REG_PROFILE7, _AD9910_REG_FTW class AD9914(Module): def __init__(self, pads, nchannels, onehot=False, **kwargs): @@ -54,3 +59,121 @@ class AD9914(Module): self.sync.rio_phy += If(current_address == 2**len(pads.a), [ If(selected(c), probe.eq(ftw)) for c, (probe, ftw) in enumerate(zip(self.probes, ftws))]) + + +class UrukulMonitor(Module): + def __init__(self, spi_phy, io_update_phy, dds, nchannels=4): + self.spi_phy = spi_phy + self.io_update_phy = io_update_phy + + self.probes = [Signal(32) for i in range(nchannels)] + + self.cs = Signal(8) + self.current_data = Signal.like(self.spi_phy.rtlink.o.data) + current_address = Signal.like(self.spi_phy.rtlink.o.address) + data_length = Signal(8) + flags = Signal(8) + + self.sync.rio += If(self.spi_phy.rtlink.o.stb, [ + current_address.eq(self.spi_phy.rtlink.o.address), + self.current_data.eq(self.spi_phy.rtlink.o.data), + If(self.spi_phy.rtlink.o.address == SPI_CONFIG_ADDR, [ + self.cs.eq(self.spi_phy.rtlink.o.data[24:]), + data_length.eq(self.spi_phy.rtlink.o.data[8:16] + 1), + flags.eq(self.spi_phy.rtlink.o.data[0:8]) + ]) + ]) + + for i in range(nchannels): + ch_sel = Signal() + self.comb += ch_sel.eq( + ((self.cs == CS_DDS_MULTI) | (self.cs == i + CS_DDS_CH0)) + & (current_address == SPI_DATA_ADDR) + ) + + if dds == "ad9912": + mon_cls = _AD9912Monitor + elif dds == "ad9910": + mon_cls = _AD9910Monitor + else: + raise NotImplementedError + + monitor = mon_cls(self.current_data, data_length, flags, ch_sel) + self.submodules += monitor + + self.sync.rio_phy += [ + If(ch_sel & self.is_io_update(), 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] + 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 + return phy_io_upd | reg_io_upd + + +class _AD9912Monitor(Module): + def __init__(self, current_data, data_length, flags, ch_sel): + self.ftw = Signal(32, reset_less=True) + + fsm = ClockDomainsRenamer("rio_phy")(FSM(reset_state="IDLE")) + self.submodules += fsm + + reg_addr = current_data[16:29] + reg_write = ~current_data[31] + + fsm.act("IDLE", + If(ch_sel & reg_write, + If((data_length == 16) & (reg_addr == AD9912_POW1), + NextState("READ") + ) + ) + ) + + fsm.act("READ", + If(ch_sel, + If(flags & SPI_END, + # lower 16 bits (16-32 from 48-bit transfer) + NextValue(self.ftw[:16], current_data[16:]), + NextState("IDLE") + ).Else( + NextValue(self.ftw[16:], current_data[:16]) + ) + ) + ) + + +class _AD9910Monitor(Module): + def __init__(self, current_data, data_length, flags, ch_sel): + self.ftw = Signal(32, reset_less=True) + + fsm = ClockDomainsRenamer("rio_phy")(FSM(reset_state="IDLE")) + self.submodules += fsm + + reg_addr = current_data[24:29] + reg_write = ~current_data[31] + + fsm.act("IDLE", + If(ch_sel & reg_write, + If((data_length == 8) & (_AD9910_REG_PROFILE7 >= reg_addr) & (reg_addr >= _AD9910_REG_PROFILE0), + NextState("READ") + ).Elif(reg_addr == _AD9910_REG_FTW, + If((data_length == 24) & (flags & SPI_END), + NextValue(self.ftw[:16], current_data[8:24]) + ).Elif(data_length == 8, + NextState("READ") + ) + ) + ) + ) + + fsm.act("READ", + If(ch_sel, + If(flags & SPI_END, + NextValue(self.ftw, current_data), + NextState("IDLE") + ) + ) + ) diff --git a/artiq/gateware/targets/kasli.py b/artiq/gateware/targets/kasli.py index 71ee933a5..5938fc947 100755 --- a/artiq/gateware/targets/kasli.py +++ b/artiq/gateware/targets/kasli.py @@ -282,6 +282,9 @@ class MasterBase(MiniSoC, AMPSoC): platform = self.platform if platform.hw_rev == "v2.0": + self.submodules.error_led = gpio.GPIOOut(Cat( + self.platform.request("error_led"))) + self.csr_devices.append("error_led") self.submodules += SMAClkinForward(platform) i2c = self.platform.request("i2c") @@ -459,6 +462,11 @@ class SatelliteBase(BaseSoC): platform = self.platform + if self.platform.hw_rev == "v2.0": + self.submodules.error_led = gpio.GPIOOut(Cat( + self.platform.request("error_led"))) + self.csr_devices.append("error_led") + disable_cdr_clk_ibuf = Signal(reset=1) disable_cdr_clk_ibuf.attr.add("no_retiming") if self.platform.hw_rev == "v2.0": diff --git a/artiq/language/environment.py b/artiq/language/environment.py index 969664d15..9ce7034cc 100644 --- a/artiq/language/environment.py +++ b/artiq/language/environment.py @@ -209,18 +209,28 @@ class TraceArgumentManager: self.requested_args[key] = processor, group, tooltip return None + def check_unprocessed_arguments(self): + pass class ProcessArgumentManager: def __init__(self, unprocessed_arguments): self.unprocessed_arguments = unprocessed_arguments + self._processed_arguments = set() def get(self, key, processor, group, tooltip): if key in self.unprocessed_arguments: r = processor.process(self.unprocessed_arguments[key]) + self._processed_arguments.add(key) else: r = processor.default() return r + def check_unprocessed_arguments(self): + unprocessed = set(self.unprocessed_arguments.keys()) -\ + self._processed_arguments + if unprocessed: + raise AttributeError("Invalid argument(s): " + + ", ".join(unprocessed)) class HasEnvironment: """Provides methods to manage the environment of an experiment (arguments, @@ -242,6 +252,8 @@ class HasEnvironment: self.__in_build = True self.build(*args, **kwargs) self.__in_build = False + if self.__argument_mgr is not None: + self.__argument_mgr.check_unprocessed_arguments() def register_child(self, child): self.children.append(child) @@ -485,7 +497,7 @@ def is_experiment(o): def is_public_experiment(o): - """Checks if a Pyhton object is a top-level, + """Checks if a Python object is a top-level, non underscore-prefixed, experiment class. """ return is_experiment(o) and not o.__name__.startswith("_") diff --git a/artiq/master/scheduler.py b/artiq/master/scheduler.py index d6d44acef..eea832a17 100644 --- a/artiq/master/scheduler.py +++ b/artiq/master/scheduler.py @@ -490,3 +490,13 @@ class Scheduler: return False return r.priority_key() > run.priority_key() raise KeyError("RID not found") + + def check_termination(self, rid): + """Returns ``True`` if termination is requested.""" + for pipeline in self._pipelines.values(): + if rid in pipeline.pool.runs: + run = pipeline.pool.runs[rid] + if run.termination_requested: + return True + return False + \ No newline at end of file diff --git a/artiq/master/worker_impl.py b/artiq/master/worker_impl.py index fdf876633..87d448cd9 100644 --- a/artiq/master/worker_impl.py +++ b/artiq/master/worker_impl.py @@ -111,6 +111,12 @@ class Scheduler: rid = self.rid return self._check_pause(rid) + _check_termination = staticmethod(make_parent_action("scheduler_check_termination")) + def check_termination(self, rid=None) -> bool: + if rid is None: + rid = self.rid + return self._check_termination(rid) + _submit = staticmethod(make_parent_action("scheduler_submit")) def submit(self, pipeline_name=None, expid=None, priority=None, due_date=None, flush=False): if pipeline_name is None: diff --git a/flake.nix b/flake.nix index 9cf0d036a..2e6211d6d 100644 --- a/flake.nix +++ b/flake.nix @@ -15,6 +15,7 @@ outputs = { self, mozilla-overlay, sipyco, nac3, artiq-comtools, src-migen, src-misoc }: let pkgs = import nac3.inputs.nixpkgs { system = "x86_64-linux"; overlays = [ (import mozilla-overlay) ]; }; + pkgs-aarch64 = import nac3.inputs.nixpkgs { system = "aarch64-linux"; }; artiqVersionMajor = 9; artiqVersionMinor = self.sourceInfo.revCount or 0; @@ -76,6 +77,7 @@ pname = "artiq"; version = artiqVersion; src = self; + doCheck = false; preBuild = '' @@ -228,7 +230,7 @@ dontFixup = true; }; - openocd-bscanspi = let + openocd-bscanspi-f = pkgs: let bscan_spi_bitstreams-pkg = pkgs.stdenv.mkDerivation { name = "bscan_spi_bitstreams"; src = pkgs.fetchFromGitHub { @@ -301,8 +303,9 @@ in rec { packages.x86_64-linux = rec { inherit (nac3.packages.x86_64-linux) python3-mimalloc; - inherit qasync openocd-bscanspi artiq; + inherit qasync artiq; inherit migen misoc asyncserial microscope vivadoEnv vivado; + openocd-bscanspi = openocd-bscanspi-f pkgs; artiq-board-kc705-nist_clock = makeArtiqBoardPackage { target = "kc705"; variant = "nist_clock"; @@ -377,6 +380,10 @@ ]; }; + packages.aarch64-linux = { + openocd-bscanspi = openocd-bscanspi-f pkgs-aarch64; + }; + hydraJobs = { inherit (packages.x86_64-linux) artiq artiq-board-kc705-nist_clock openocd-bscanspi; sipyco-msys2-pkg = packages.x86_64-w64-mingw32.sipyco-pkg;