Merge branch 'master' into nac3

This commit is contained in:
Sebastien Bourdeauducq 2022-08-18 14:35:09 +08:00
commit 0953a07582
15 changed files with 484 additions and 64 deletions

View File

@ -3,48 +3,66 @@
Release notes Release notes
============= =============
Unreleased
----------
Highlights:
* Implemented Phaser-servo. This requires recent gateware on Phaser.
ARTIQ-7 ARTIQ-7
------- -------
Highlights: Highlights:
* New hardware support: * 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 - HVAMP_8CH 8 channel HV amplifier for Fastino / Zotinos
- Almazny mezzanine board for Mirny - 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). * Softcore targets now use the RISC-V architecture (VexRiscv) instead of OR1K (mor1kx).
* Gateware FPU is supported on KC705 and Kasli 2.0. * Gateware FPU is supported on KC705 and Kasli 2.0.
* Faster compilation for large arrays/lists. * Faster compilation for large arrays/lists.
* Phaser: * Faster exception handling.
- Improved documentation * Several exception handling bugs fixed.
- Expose the DAC coarse mixer and ``sif_sync`` * Support for a simpler shared library system with faster calls into the runtime. This is only used by the NAC3
- Exposes upconverter calibration and enabling/disabling of upconverter LO & RF outputs. compiler (nac3ld) and improves RTIO output performance (test_pulse_rate) by 9-10%.
- Add helpers to align Phaser updates to the RTIO timeline (``get_next_frame_mu()``) * Moninj improvements:
* Core device moninj is now proxied via the ``aqctl_moninj_proxy`` controller. - 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 * The configuration entry ``rtio_clock`` supports multiple clocking settings, deprecating the usage
of compile-time options. of compile-time options.
* Packaging via Nix Flakes. * Added support for 100MHz RTIO clock in DRTIO.
* Firmware and gateware can now be built on-demand on the M-Labs server using ``afws_client`` * Previously detected RTIO async errors are reported to the host after each kernel terminates and a
(subscribers only). 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. * 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 * 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 * ``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``). 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 * ``artiq_master`` now has an ``--experiment-subdir`` option to scan only a subdirectory of the
repository when building the list of experiments. repository when building the list of experiments.
* Added support for 100MHz RTIO clock in DRTIO. * Experiments can now be submitted by-content.
* Previously detected RTIO async errors are reported to the host after each kernel terminates and a * The master can now optionally log all experiments submitted into a CSV file.
warning is logged. The warning is additional to the one already printed in the core device log upon
detection of the error.
* Removed worker DB warning for writing a dataset that is also in the archive. * 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 * Experiments can now call ``scheduler.check_termination()`` to test if the user
possibly other switches in future. Readback has been removed, and now only one channel per has requested graceful termination.
switch is supported. * ARTIQ command-line programs and controllers now exit cleanly on Ctrl-C.
* The "ip" config option can now be set to "use_dhcp" in order to use DHCP to obtain an IP address. * ``artiq_coremgmt reboot`` now reloads gateware as well, providing a more thorough and reliable
DHCP will also be used if no "ip" config option is set. 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: Breaking changes:
@ -59,6 +77,10 @@ Breaking changes:
* Mirny: Added extra delays in ``ADF5356.sync()``. This avoids the need of an extra delay before * Mirny: Added extra delays in ``ADF5356.sync()``. This avoids the need of an extra delay before
calling `ADF5356.init()`. calling `ADF5356.init()`.
* The deprecated ``set_dataset(..., save=...)`` is no longer supported. * 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 ARTIQ-6
------- -------

View File

@ -41,6 +41,14 @@ PHASER_ADDR_DUC1_P = 0x26
PHASER_ADDR_DAC1_DATA = 0x28 PHASER_ADDR_DAC1_DATA = 0x28
PHASER_ADDR_DAC1_TEST = 0x2c 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_DAC = 1 << 0
PHASER_SEL_TRF0 = 1 << 1 PHASER_SEL_TRF0 = 1 << 1
PHASER_SEL_TRF1 = 1 << 2 PHASER_SEL_TRF1 = 1 << 2
@ -59,6 +67,11 @@ PHASER_DAC_SEL_TEST = 1
PHASER_HW_REV_VARIANT = 1 << 4 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 @nac3
class Phaser: class Phaser:
@ -114,6 +127,31 @@ class Phaser:
configured through a shared SPI bus that is accessed and controlled via configured through a shared SPI bus that is accessed and controlled via
FPGA registers. 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 .. note:: Various register settings of the DAC and the quadrature
upconverters are available to be modified through the `dac`, `trf0`, upconverters are available to be modified through the `dac`, `trf0`,
`trf1` dictionaries. These can be set through the device database `trf1` dictionaries. These can be set through the device database
@ -322,6 +360,8 @@ class Phaser:
self.core.delay(.1*ms) self.core.delay(.1*ms)
channel.set_att_mu(0x00) # minimum attenuation channel.set_att_mu(0x00) # minimum attenuation
channel.set_servo(profile=0, enable=False, hold=True)
# test oscillators and DUC # test oscillators and DUC
for i in range(len(channel.oscillator)): for i in range(len(channel.oscillator)):
oscillator = channel.oscillator[i] oscillator = channel.oscillator[i]
@ -394,6 +434,12 @@ class Phaser:
response = rtio_input_data(self.channel_base) response = rtio_input_data(self.channel_base)
return response >> self.miso_delay 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 @kernel
def write32(self, addr: int32, data: int32): def write32(self, addr: int32, data: int32):
"""Write 32 bit to a sequence of FPGA registers.""" """Write 32 bit to a sequence of FPGA registers."""
@ -1059,6 +1105,133 @@ class PhaserChannel:
data = data ^ ((1 << 12) | (1 << 13)) data = data ^ ((1 << 12) | (1 << 13))
self.trf_write(data) 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 @nac3
class PhaserOscillator: class PhaserOscillator:

View File

@ -8,6 +8,8 @@ from PyQt5 import QtCore, QtWidgets, QtGui
from sipyco.sync_struct import Subscriber from sipyco.sync_struct import Subscriber
from artiq.coredevice.comm_moninj import * 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.tools import LayoutWidget
from artiq.gui.flowlayout import FlowLayout from artiq.gui.flowlayout import FlowLayout
@ -179,14 +181,45 @@ class _SimpleDisplayWidget(QtWidgets.QFrame):
raise NotImplementedError 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): 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.dm = dm
self.bus_channel = bus_channel self.bus_channel = bus_channel
self.channel = channel self.channel = channel
self.dds_name = title self.dds_name = title
self.cpld = cpld
self.cur_frequency = 0 self.cur_frequency = 0
self.dds_model = dds_model
QtWidgets.QFrame.__init__(self) QtWidgets.QFrame.__init__(self)
@ -249,7 +282,7 @@ class _DDSWidget(QtWidgets.QFrame):
set_grid.addWidget(set_btn, 0, 1, 1, 1) set_grid.addWidget(set_btn, 0, 1, 1, 1)
# for urukuls also allow switching off RF # for urukuls also allow switching off RF
if self.cpld: if self.dds_model.is_urukul:
off_btn = QtWidgets.QToolButton() off_btn = QtWidgets.QToolButton()
off_btn.setText("Off") off_btn.setText("Off")
off_btn.setToolTip("Switch off the output") off_btn.setToolTip("Switch off the output")
@ -276,7 +309,7 @@ class _DDSWidget(QtWidgets.QFrame):
set_btn.clicked.connect(self.set_clicked) set_btn.clicked.connect(self.set_clicked)
apply.clicked.connect(self.apply_changes) apply.clicked.connect(self.apply_changes)
if self.cpld: if self.dds_model.is_urukul:
off_btn.clicked.connect(self.off_clicked) off_btn.clicked.connect(self.off_clicked)
self.value_edit.returnPressed.connect(lambda: self.apply_changes(None)) self.value_edit.returnPressed.connect(lambda: self.apply_changes(None))
self.value_edit.escapePressedConnect(self.cancel_changes) self.value_edit.escapePressedConnect(self.cancel_changes)
@ -293,19 +326,20 @@ class _DDSWidget(QtWidgets.QFrame):
self.value_edit.selectAll() self.value_edit.selectAll()
def off_clicked(self, set): 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): def apply_changes(self, apply):
self.data_stack.setCurrentIndex(0) self.data_stack.setCurrentIndex(0)
self.button_stack.setCurrentIndex(0) self.button_stack.setCurrentIndex(0)
frequency = float(self.value_edit.text())*1e6 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): def cancel_changes(self, cancel):
self.data_stack.setCurrentIndex(0) self.data_stack.setCurrentIndex(0)
self.button_stack.setCurrentIndex(0) self.button_stack.setCurrentIndex(0)
def refresh_display(self): def refresh_display(self):
self.cur_frequency = self.dds_model.cur_frequency
self.value_label.setText("<font size=\"4\">{:.7f}</font>" self.value_label.setText("<font size=\"4\">{:.7f}</font>"
.format(self.cur_frequency/1e6)) .format(self.cur_frequency/1e6))
self.value_edit.setText("{:.7f}" self.value_edit.setText("{:.7f}"
@ -356,7 +390,8 @@ def setup_from_ddb(ddb):
bus_channel = v["arguments"]["bus_channel"] bus_channel = v["arguments"]["bus_channel"]
channel = v["arguments"]["channel"] channel = v["arguments"]["channel"]
dds_sysclk = v["arguments"]["sysclk"] 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) description.add(widget)
elif (v["module"] == "artiq.coredevice.ad9910" elif (v["module"] == "artiq.coredevice.ad9910"
and v["class"] == "AD9910") or \ and v["class"] == "AD9910") or \
@ -368,7 +403,11 @@ def setup_from_ddb(ddb):
dds_cpld = v["arguments"]["cpld_device"] dds_cpld = v["arguments"]["cpld_device"]
spi_dev = ddb[dds_cpld]["arguments"]["spi_device"] spi_dev = ddb[dds_cpld]["arguments"]["spi_device"]
bus_channel = ddb[spi_dev]["arguments"]["channel"] 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) description.add(widget)
elif ( (v["module"] == "artiq.coredevice.ad53xx" and v["class"] == "AD53xx") elif ( (v["module"] == "artiq.coredevice.ad53xx" and v["class"] == "AD53xx")
or (v["module"] == "artiq.coredevice.zotino" and v["class"] == "Zotino")): 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) mi_port = v.get("port_proxy", 1383)
except KeyError: except KeyError:
pass pass
return mi_addr, mi_port, dds_sysclk, description return mi_addr, mi_port, description
class _DeviceManager: class _DeviceManager:
@ -415,15 +454,13 @@ class _DeviceManager:
return ddb return ddb
def notify(self, mod): 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): if (mi_addr, mi_port) != (self.mi_addr, self.mi_port):
self.mi_addr = mi_addr self.mi_addr = mi_addr
self.mi_port = mi_port self.mi_port = mi_port
self.reconnect_mi.set() self.reconnect_mi.set()
self.dds_sysclk = dds_sysclk
for to_remove in self.description - description: for to_remove in self.description - description:
widget = self.widgets_by_uid[to_remove.uid] widget = self.widgets_by_uid[to_remove.uid]
del 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, "log_level": logging.WARNING,
"content": content, "content": content,
"class_name": class_name, "class_name": class_name,
"arguments": [] "arguments": {}
} }
scheduling = { scheduling = {
"pipeline_name": "main", "pipeline_name": "main",
@ -512,24 +549,25 @@ class _DeviceManager:
scheduling["flush"]) scheduling["flush"])
logger.info("Submitted '%s', RID is %d", title, rid) 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 # 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 # urukuls need CPLD init and switch to on
# keep previous config if it was set already # keep previous config if it was set already
cpld_dev = """self.setattr_device("core_cache") 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") cpld_init = """cfg = self.core_cache.get("_{cpld}_cfg")
if len(cfg) > 0: if len(cfg) > 0:
self.{cpld}.cfg_reg = cfg[0] self.{cpld}.cfg_reg = cfg[0]
else: else:
delay(15*ms)
self.{cpld}.init() self.{cpld}.init()
self.core_cache.put("_{cpld}_cfg", [self.{cpld}.cfg_reg]) self.core_cache.put("_{cpld}_cfg", [self.{cpld}.cfg_reg])
cfg = self.core_cache.get("_{cpld}_cfg") cfg = self.core_cache.get("_{cpld}_cfg")
""".format(cpld=dds_cpld) """.format(cpld=dds_model.cpld)
cfg_sw = """self.{}.cfg_sw(True) cfg_sw = """self.{}.cfg_sw(True)
cfg[0] = self.{}.cfg_reg cfg[0] = self.{}.cfg_reg
""".format(dds_channel, dds_cpld) """.format(dds_channel, dds_model.cpld)
else: else:
cpld_dev = "" cpld_dev = ""
cpld_init = "" cpld_init = ""
@ -546,8 +584,8 @@ class _DeviceManager:
@kernel @kernel
def run(self): def run(self):
self.core.break_realtime() self.core.break_realtime()
delay(2*ms)
{cpld_init} {cpld_init}
delay(5*ms)
self.{dds_channel}.init() self.{dds_channel}.init()
self.{dds_channel}.set({freq}) self.{dds_channel}.set({freq})
{cfg_sw} {cfg_sw}
@ -560,7 +598,7 @@ class _DeviceManager:
"SetDDS", "SetDDS",
"Set DDS {} {}MHz".format(dds_channel, freq/1e6))) "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 # urukul only
toggle_exp = textwrap.dedent(""" toggle_exp = textwrap.dedent("""
from artiq.experiment import * from artiq.experiment import *
@ -575,18 +613,19 @@ class _DeviceManager:
@kernel @kernel
def run(self): def run(self):
self.core.break_realtime() self.core.break_realtime()
delay(2*ms)
cfg = self.core_cache.get("_{cpld}_cfg") cfg = self.core_cache.get("_{cpld}_cfg")
if len(cfg) > 0: if len(cfg) > 0:
self.{cpld}.cfg_reg = cfg[0] self.{cpld}.cfg_reg = cfg[0]
else: else:
delay(15*ms)
self.{cpld}.init() self.{cpld}.init()
self.core_cache.put("_{cpld}_cfg", [self.{cpld}.cfg_reg]) self.core_cache.put("_{cpld}_cfg", [self.{cpld}.cfg_reg])
cfg = self.core_cache.get("_{cpld}_cfg") cfg = self.core_cache.get("_{cpld}_cfg")
delay(5*ms)
self.{ch}.init() self.{ch}.init()
self.{ch}.cfg_sw({sw}) self.{ch}.cfg_sw({sw})
cfg[0] = self.{cpld}.cfg_reg 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( asyncio.ensure_future(
self._submit_by_content( self._submit_by_content(
toggle_exp, toggle_exp,
@ -619,11 +658,11 @@ class _DeviceManager:
elif probe == TTLProbe.oe.value: elif probe == TTLProbe.oe.value:
widget.cur_oe = bool(value) widget.cur_oe = bool(value)
widget.refresh_display() widget.refresh_display()
if (channel, probe) in self.dds_widgets: elif (channel, probe) in self.dds_widgets:
widget = self.dds_widgets[(channel, probe)] 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() widget.refresh_display()
if (channel, probe) in self.dac_widgets: elif (channel, probe) in self.dac_widgets:
widget = self.dac_widgets[(channel, probe)] widget = self.dac_widgets[(channel, probe)]
widget.cur_value = value widget.cur_value = value
widget.refresh_display() widget.refresh_display()
@ -656,11 +695,11 @@ class _DeviceManager:
logger.info("cancelled connection to moninj") logger.info("cancelled connection to moninj")
break break
except: 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.) await asyncio.sleep(10.)
self.reconnect_mi.set() self.reconnect_mi.set()
else: else:
logger.info("ARTIQ dashboard connected to moninj proxy (%s)", logger.info("ARTIQ dashboard connected to moninj (%s)",
self.mi_addr) self.mi_addr)
self.mi_connection = new_mi_connection self.mi_connection = new_mi_connection
for ttl_channel in self.ttl_widgets.keys(): for ttl_channel in self.ttl_widgets.keys():

View File

@ -116,6 +116,9 @@ class MonitorMux:
else: else:
raise ValueError raise ValueError
def disconnect_cb(self):
self.listeners.clear()
class ProxyConnection: class ProxyConnection:
def __init__(self, monitor_mux, reader, writer): def __init__(self, monitor_mux, reader, writer):
@ -203,7 +206,9 @@ def main():
signal_handler.setup() signal_handler.setup()
try: try:
monitor_mux = MonitorMux() 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 monitor_mux.comm_moninj = comm_moninj
loop.run_until_complete(comm_moninj.connect(args.core_addr)) loop.run_until_complete(comm_moninj.connect(args.core_addr))
try: try:

View File

@ -114,7 +114,12 @@ class Programmer:
"telnet_port disabled" "telnet_port disabled"
] + preinit_script ] + preinit_script
self._loaded = defaultdict(lambda: None) 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): def _transfer_script(self, script):
if isinstance(self._client, LocalClient): if isinstance(self._client, LocalClient):

View File

@ -128,6 +128,7 @@ def main():
"scheduler_request_termination": scheduler.request_termination, "scheduler_request_termination": scheduler.request_termination,
"scheduler_get_status": scheduler.get_status, "scheduler_get_status": scheduler.get_status,
"scheduler_check_pause": scheduler.check_pause, "scheduler_check_pause": scheduler.check_pause,
"scheduler_check_termination": scheduler.check_termination,
"ccb_issue": ccb_issue, "ccb_issue": ccb_issue,
}) })
experiment_db.scan_repository_async() experiment_db.scan_repository_async()

View File

@ -295,7 +295,10 @@ class GTX(Module, TransceiverInterface):
i_CEB=stable_clkin_n, i_CEB=stable_clkin_n,
i_I=clock_pads.p, i_I=clock_pads.p,
i_IB=clock_pads.n, 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() rtio_tx_clk = Signal()

View File

@ -3,7 +3,7 @@ from migen.build.generic_platform import *
from migen.genlib.io import DifferentialOutput from migen.genlib.io import DifferentialOutput
from artiq.gateware import rtio 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.suservo import servo, pads as servo_pads
from artiq.gateware.rtio.phy import servo as rtservo, fastino, phaser from artiq.gateware.rtio.phy import servo as rtservo, fastino, phaser
@ -222,13 +222,13 @@ class Urukul(_EEM):
return ios return ios
@classmethod @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) 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.platform.request("urukul{}_spi_n".format(eem)))
target.submodules += phy target.submodules += spi_phy
target.rtio_channels.append(rtio.Channel.from_phy(phy, ififo_depth=4)) target.rtio_channels.append(rtio.Channel.from_phy(spi_phy, ififo_depth=4))
pads = target.platform.request("urukul{}_dds_reset_sync_in".format(eem)) 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 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)) target.rtio_channels.append(rtio.Channel.from_phy(phy))
pads = target.platform.request("urukul{}_io_update".format(eem)) pads = target.platform.request("urukul{}_io_update".format(eem))
phy = ttl_out_cls(pads.p, pads.n) io_upd_phy = ttl_out_cls(pads.p, pads.n)
target.submodules += phy target.submodules += io_upd_phy
target.rtio_channels.append(rtio.Channel.from_phy(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: if eem_aux is not None:
for signal in "sw0 sw1 sw2 sw3".split(): for signal in "sw0 sw1 sw2 sw3".split():
pads = target.platform.request("urukul{}_{}".format(eem, signal)) pads = target.platform.request("urukul{}_{}".format(eem, signal))
@ -247,6 +252,7 @@ class Urukul(_EEM):
target.submodules += phy target.submodules += phy
target.rtio_channels.append(rtio.Channel.from_phy(phy)) target.rtio_channels.append(rtio.Channel.from_phy(phy))
class Sampler(_EEM): class Sampler(_EEM):
@staticmethod @staticmethod
def io(eem, eem_aux, iostandard): def io(eem, eem_aux, iostandard):

View File

@ -47,7 +47,7 @@ def peripheral_urukul(module, peripheral, **kwargs):
else: else:
sync_gen_cls = None sync_gen_cls = None
eem.Urukul.add_std(module, port, port_aux, ttl_serdes_7series.Output_8X, 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): def peripheral_sampler(module, peripheral, **kwargs):

View File

@ -3,6 +3,11 @@ from migen import *
from artiq.gateware import ad9_dds from artiq.gateware import ad9_dds
from artiq.gateware.rtio.phy.wishbone import RT2WB 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): class AD9914(Module):
def __init__(self, pads, nchannels, onehot=False, **kwargs): 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), [ self.sync.rio_phy += If(current_address == 2**len(pads.a), [
If(selected(c), probe.eq(ftw)) If(selected(c), probe.eq(ftw))
for c, (probe, ftw) in enumerate(zip(self.probes, ftws))]) 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")
)
)
)

View File

@ -282,6 +282,9 @@ class MasterBase(MiniSoC, AMPSoC):
platform = self.platform platform = self.platform
if platform.hw_rev == "v2.0": 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) self.submodules += SMAClkinForward(platform)
i2c = self.platform.request("i2c") i2c = self.platform.request("i2c")
@ -459,6 +462,11 @@ class SatelliteBase(BaseSoC):
platform = self.platform 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 = Signal(reset=1)
disable_cdr_clk_ibuf.attr.add("no_retiming") disable_cdr_clk_ibuf.attr.add("no_retiming")
if self.platform.hw_rev == "v2.0": if self.platform.hw_rev == "v2.0":

View File

@ -209,18 +209,28 @@ class TraceArgumentManager:
self.requested_args[key] = processor, group, tooltip self.requested_args[key] = processor, group, tooltip
return None return None
def check_unprocessed_arguments(self):
pass
class ProcessArgumentManager: class ProcessArgumentManager:
def __init__(self, unprocessed_arguments): def __init__(self, unprocessed_arguments):
self.unprocessed_arguments = unprocessed_arguments self.unprocessed_arguments = unprocessed_arguments
self._processed_arguments = set()
def get(self, key, processor, group, tooltip): def get(self, key, processor, group, tooltip):
if key in self.unprocessed_arguments: if key in self.unprocessed_arguments:
r = processor.process(self.unprocessed_arguments[key]) r = processor.process(self.unprocessed_arguments[key])
self._processed_arguments.add(key)
else: else:
r = processor.default() r = processor.default()
return r 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: class HasEnvironment:
"""Provides methods to manage the environment of an experiment (arguments, """Provides methods to manage the environment of an experiment (arguments,
@ -242,6 +252,8 @@ class HasEnvironment:
self.__in_build = True self.__in_build = True
self.build(*args, **kwargs) self.build(*args, **kwargs)
self.__in_build = False self.__in_build = False
if self.__argument_mgr is not None:
self.__argument_mgr.check_unprocessed_arguments()
def register_child(self, child): def register_child(self, child):
self.children.append(child) self.children.append(child)
@ -485,7 +497,7 @@ def is_experiment(o):
def is_public_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. non underscore-prefixed, experiment class.
""" """
return is_experiment(o) and not o.__name__.startswith("_") return is_experiment(o) and not o.__name__.startswith("_")

View File

@ -490,3 +490,13 @@ class Scheduler:
return False return False
return r.priority_key() > run.priority_key() return r.priority_key() > run.priority_key()
raise KeyError("RID not found") 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

View File

@ -111,6 +111,12 @@ class Scheduler:
rid = self.rid rid = self.rid
return self._check_pause(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")) _submit = staticmethod(make_parent_action("scheduler_submit"))
def submit(self, pipeline_name=None, expid=None, priority=None, due_date=None, flush=False): def submit(self, pipeline_name=None, expid=None, priority=None, due_date=None, flush=False):
if pipeline_name is None: if pipeline_name is None:

View File

@ -15,6 +15,7 @@
outputs = { self, mozilla-overlay, sipyco, nac3, artiq-comtools, src-migen, src-misoc }: outputs = { self, mozilla-overlay, sipyco, nac3, artiq-comtools, src-migen, src-misoc }:
let let
pkgs = import nac3.inputs.nixpkgs { system = "x86_64-linux"; overlays = [ (import mozilla-overlay) ]; }; pkgs = import nac3.inputs.nixpkgs { system = "x86_64-linux"; overlays = [ (import mozilla-overlay) ]; };
pkgs-aarch64 = import nac3.inputs.nixpkgs { system = "aarch64-linux"; };
artiqVersionMajor = 9; artiqVersionMajor = 9;
artiqVersionMinor = self.sourceInfo.revCount or 0; artiqVersionMinor = self.sourceInfo.revCount or 0;
@ -76,6 +77,7 @@
pname = "artiq"; pname = "artiq";
version = artiqVersion; version = artiqVersion;
src = self; src = self;
doCheck = false;
preBuild = preBuild =
'' ''
@ -228,7 +230,7 @@
dontFixup = true; dontFixup = true;
}; };
openocd-bscanspi = let openocd-bscanspi-f = pkgs: let
bscan_spi_bitstreams-pkg = pkgs.stdenv.mkDerivation { bscan_spi_bitstreams-pkg = pkgs.stdenv.mkDerivation {
name = "bscan_spi_bitstreams"; name = "bscan_spi_bitstreams";
src = pkgs.fetchFromGitHub { src = pkgs.fetchFromGitHub {
@ -301,8 +303,9 @@
in rec { in rec {
packages.x86_64-linux = rec { packages.x86_64-linux = rec {
inherit (nac3.packages.x86_64-linux) python3-mimalloc; inherit (nac3.packages.x86_64-linux) python3-mimalloc;
inherit qasync openocd-bscanspi artiq; inherit qasync artiq;
inherit migen misoc asyncserial microscope vivadoEnv vivado; inherit migen misoc asyncserial microscope vivadoEnv vivado;
openocd-bscanspi = openocd-bscanspi-f pkgs;
artiq-board-kc705-nist_clock = makeArtiqBoardPackage { artiq-board-kc705-nist_clock = makeArtiqBoardPackage {
target = "kc705"; target = "kc705";
variant = "nist_clock"; variant = "nist_clock";
@ -377,6 +380,10 @@
]; ];
}; };
packages.aarch64-linux = {
openocd-bscanspi = openocd-bscanspi-f pkgs-aarch64;
};
hydraJobs = { hydraJobs = {
inherit (packages.x86_64-linux) artiq artiq-board-kc705-nist_clock openocd-bscanspi; inherit (packages.x86_64-linux) artiq artiq-board-kc705-nist_clock openocd-bscanspi;
sipyco-msys2-pkg = packages.x86_64-w64-mingw32.sipyco-pkg; sipyco-msys2-pkg = packages.x86_64-w64-mingw32.sipyco-pkg;