From 5ef94d30ddefed40235965d6f37c7fd570783d57 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Thu, 18 Aug 2022 14:35:58 +0800 Subject: [PATCH 01/72] versioneer: fix default --- versioneer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versioneer.py b/versioneer.py index 0c8fbaf35..f6393d21e 100644 --- a/versioneer.py +++ b/versioneer.py @@ -11,7 +11,7 @@ def get_rev(): """ def get_version(): - return os.getenv("VERSIONEER_OVERRIDE", default="7.0.beta") + return os.getenv("VERSIONEER_OVERRIDE", default="8.0.beta") def get_rev(): return os.getenv("VERSIONEER_REV", default="unknown") From 20cb99061e5c2f30c481630ec9ccc35e82a113f5 Mon Sep 17 00:00:00 2001 From: fanmingyu212 Date: Wed, 24 Aug 2022 12:32:39 -0700 Subject: [PATCH 02/72] doc: updates artiq_flash syntax in developing.rst --- doc/manual/developing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/developing.rst b/doc/manual/developing.rst index 7435cd1cc..f2402e789 100644 --- a/doc/manual/developing.rst +++ b/doc/manual/developing.rst @@ -16,7 +16,7 @@ ARTIQ itself does not depend on Nix, and it is also possible to compile everythi * Enable flakes in Nix by e.g. adding ``experimental-features = nix-command flakes`` to ``nix.conf`` (for example ``~/.config/nix/nix.conf``). * Enter the development shell by running ``nix develop git+https://github.com/m-labs/artiq.git``, or alternatively by cloning the ARTIQ Git repository and running ``nix develop`` at the root (where ``flake.nix`` is). * You can then build the firmware and gateware with a command such as ``$ python -m artiq.gateware.targets.kasli``. If you are using a JSON system description file, use ``$ python -m artiq.gateware.targets.kasli_generic file.json``. -* Flash the binaries into the FPGA board with a command such as ``$ artiq_flash --srcbuild -d artiq_kasli -V ``. You need to configure OpenOCD as explained :ref:`in the user section `. OpenOCD is already part of the flake's development environment. +* Flash the binaries into the FPGA board with a command such as ``$ artiq_flash --srcbuild -d artiq_kasli/``. You need to configure OpenOCD as explained :ref:`in the user section `. OpenOCD is already part of the flake's development environment. * Check that the board boots and examine the UART messages by running a serial terminal program, e.g. ``$ flterm /dev/ttyUSB1`` (``flterm`` is part of MiSoC and installed in the flake's development environment). Leave the terminal running while you are flashing the board, so that you see the startup messages when the board boots immediately after flashing. You can also restart the board (without reflashing it) with ``$ artiq_flash start``. * The communication parameters are 115200 8-N-1. Ensure that your user has access to the serial device (e.g. by adding the user account to the ``dialout`` group). From b705862ecd014b0c3b69051c1c822f13ce8aeea2 Mon Sep 17 00:00:00 2001 From: mwojcik Date: Thu, 25 Aug 2022 12:08:05 +0800 Subject: [PATCH 03/72] afws_client: fix argument order --- doc/manual/installing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/installing.rst b/doc/manual/installing.rst index 1695aeac6..8effbf2d9 100644 --- a/doc/manual/installing.rst +++ b/doc/manual/installing.rst @@ -219,7 +219,7 @@ If you have an active firmware subscription with M-Labs or QUARTIQ, you can obta Run the command:: - $ afws_client [username] build [variant] [afws_directory] + $ afws_client [username] build [afws_directory] [variant] Replace ``[username]`` with the login name that was given to you with the subscription, ``[variant]`` with the name of your system variant, and ``[afws_directory]`` with the name of an empty directory, which will be created by the command if it does not exist. Enter your password when prompted and wait for the build (if applicable) and download to finish. If you experience issues with the AFWS client, write to the helpdesk@ email. From 7c306d5609ed6d19d72a0489146e90665404fdfa Mon Sep 17 00:00:00 2001 From: Deepskyhunter <48083317+Deepskyhunter@users.noreply.github.com> Date: Mon, 29 Aug 2022 15:20:44 +0800 Subject: [PATCH 04/72] GUI log: Apply level and text filter to existing log messages (#1950) --- artiq/gui/log.py | 58 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/artiq/gui/log.py b/artiq/gui/log.py index 9bf9d7efa..6a8ddecad 100644 --- a/artiq/gui/log.py +++ b/artiq/gui/log.py @@ -18,6 +18,32 @@ class _ModelItem: self.children_by_row = [] +class _LogFilterProxyModel(QtCore.QSortFilterProxyModel): + def __init__(self): + super().__init__() + self.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + self.setRecursiveFilteringEnabled(True) + self.filter_level = 0 + + def filterAcceptsRow(self, source_row, source_parent): + source = self.sourceModel() + index0 = source.index(source_row, 0, source_parent) + index1 = source.index(source_row, 1, source_parent) + level = source.data(index0, QtCore.Qt.UserRole) + + if level >= self.filter_level: + regex = self.filterRegExp() + index0_text = source.data(index0, QtCore.Qt.DisplayRole) + msg_text = source.data(index1, QtCore.Qt.DisplayRole) + return (regex.indexIn(index0_text) != -1 or regex.indexIn(msg_text) != -1) + else: + return False + + def apply_filter_level(self, filter_level): + self.filter_level = getattr(logging, filter_level) + self.invalidateFilter() + + class _Model(QtCore.QAbstractItemModel): def __init__(self): QtCore.QAbstractTableModel.__init__(self) @@ -168,6 +194,8 @@ class _Model(QtCore.QAbstractItemModel): return (log_level_to_name(v[0]) + ", " + time.strftime("%m/%d %H:%M:%S", time.localtime(v[2])) + "\n" + v[3][lineno]) + elif role == QtCore.Qt.UserRole: + return self.entries[msgnum][0] class LogDock(QDockWidgetCloseDetect): @@ -240,27 +268,22 @@ class LogDock(QDockWidgetCloseDetect): self.log.header().resizeSection(0, 26*cw) self.model = _Model() - self.log.setModel(self.model) + self.proxy_model = _LogFilterProxyModel() + self.proxy_model.setSourceModel(self.model) + self.log.setModel(self.proxy_model) + self.model.rowsAboutToBeInserted.connect(self.rows_inserted_before) self.model.rowsInserted.connect(self.rows_inserted_after) self.model.rowsRemoved.connect(self.rows_removed) - def append_message(self, msg): - min_level = getattr(logging, self.filter_level.currentText()) - freetext = self.filter_freetext.text() + self.filter_freetext.returnPressed.connect(self.apply_text_filter) + self.filter_level.currentIndexChanged.connect(self.apply_level_filter) - accepted_level = msg[0] >= min_level + def apply_text_filter(self): + self.proxy_model.setFilterRegExp(self.filter_freetext.text()) - if freetext: - data_source = msg[1] - data_message = msg[3] - accepted_freetext = (freetext in data_source - or any(freetext in m for m in data_message)) - else: - accepted_freetext = True - - if accepted_level and accepted_freetext: - self.model.append(msg) + def apply_level_filter(self): + self.proxy_model.apply_filter_level(self.filter_level.currentText()) def scroll_to_bottom(self): self.log.scrollToBottom() @@ -291,7 +314,8 @@ class LogDock(QDockWidgetCloseDetect): def copy_to_clipboard(self): idx = self.log.selectedIndexes() if idx: - entry = "\n".join(self.model.full_entry(idx[0])) + source_idx = self.proxy_model.mapToSource(idx[0]) + entry = "\n".join(self.model.full_entry(source_idx)) QtWidgets.QApplication.clipboard().setText(entry) def save_state(self): @@ -331,7 +355,7 @@ class LogDockManager: def append_message(self, msg): for dock in self.docks.values(): - dock.append_message(msg) + dock.model.append(msg) def create_new_dock(self, add_to_area=True): n = 0 From 3c7ab498d18542ea5b89b64a3fb6dae5bfec691a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Sowi=C5=84ski?= Date: Thu, 1 Sep 2022 14:24:15 +0200 Subject: [PATCH 05/72] Added DDS selection for Kasli tester variant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mikołaj Sowiński --- artiq/gateware/targets/kasli.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/artiq/gateware/targets/kasli.py b/artiq/gateware/targets/kasli.py index 5938fc947..7ed52052b 100755 --- a/artiq/gateware/targets/kasli.py +++ b/artiq/gateware/targets/kasli.py @@ -170,9 +170,11 @@ class Tester(StandaloneBase): """ Configuration for CI tests. Contains the maximum number of different EEMs. """ - def __init__(self, hw_rev=None, **kwargs): + def __init__(self, hw_rev=None, dds=None, **kwargs): if hw_rev is None: hw_rev = "v2.0" + if dds is None: + dds = "ad9910" StandaloneBase.__init__(self, hw_rev=hw_rev, **kwargs) self.config["SI5324_AS_SYNTHESIZER"] = None @@ -186,7 +188,7 @@ class Tester(StandaloneBase): eem.DIO.add_std(self, 5, ttl_serdes_7series.InOut_8X, ttl_serdes_7series.Output_8X, edge_counter_cls=edge_counter.SimpleEdgeCounter) - eem.Urukul.add_std(self, 0, 1, ttl_serdes_7series.Output_8X, + eem.Urukul.add_std(self, 0, 1, ttl_serdes_7series.Output_8X, dds, 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) @@ -686,6 +688,9 @@ def main(): help="variant: {} (default: %(default)s)".format( "/".join(sorted(VARIANTS.keys())))) parser.add_argument("--with-wrpll", default=False, action="store_true") + parser.add_argument("--tester-dds", default=None, + help="Tester variant DDS type: ad9910/ad9912 " + "(default: ad9910)") parser.add_argument("--gateware-identifier-str", default=None, help="Override ROM identifier") args = parser.parse_args() @@ -694,6 +699,7 @@ def main(): if args.with_wrpll: argdict["with_wrpll"] = True argdict["gateware_identifier_str"] = args.gateware_identifier_str + argdict["dds"] = args.tester_dds variant = args.variant.lower() try: From 47f90a58cc7e99958042623f5428136a1518f304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 2 Sep 2022 08:59:29 +0000 Subject: [PATCH 06/72] add miqro phy --- artiq/gateware/rtio/phy/phaser.py | 81 +++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/artiq/gateware/rtio/phy/phaser.py b/artiq/gateware/rtio/phy/phaser.py index bb299ab0c..038f8b2c5 100644 --- a/artiq/gateware/rtio/phy/phaser.py +++ b/artiq/gateware/rtio/phy/phaser.py @@ -1,5 +1,6 @@ from migen import * from misoc.cores.duc import MultiDDS +from misoc.interconnect.stream import Endpoint from artiq.gateware.rtio import rtlink from .fastlink import SerDes, SerInterface @@ -87,3 +88,83 @@ class Phaser(Module): self.rtlink.i.stb.eq(re_dly[0] & self.serializer.stb), self.rtlink.i.data.eq(self.serializer.readback), ] + + +class MiqroChannel(Module): + def __init__(self, regs): + self.rtlink = rtlink.Interface( + rtlink.OInterface(data_width=30, address_width=2, fine_ts_width=1, + enable_replace=False)) + self.pulse = Signal(128) + self.ack = Signal() + regs = [Signal(30, reset_less=True) for _ in range(3)] + dt = Signal(7, reset_less=True) + stb = Signal() + self.comb += self.pulase.payload.data.eq(Cat(stb, dt, regs)) + self.sync.rtio += [ + dt.eq(dt + 2), + If(self.ack, + dt.eq(0), + stb.eq(0), + ), + If(self.rtlink.o.stb, + Array(regs)[self.rtlink.o.address].eq(self.rtlink.o.data), + If(self.rtlink.o.address == 0, + dt[0].eq(self.rtlink.o.fine_ts), + stb.eq(1), + ), + ), + ] + + +class Miqro(Module): + def __init__(self, pins, pins_n): + self.rtlink = rtlink.Interface( + rtlink.OInterface(data_width=8, address_width=8, + enable_replace=False), + rtlink.IInterface(data_width=10)) + + self.submodules.ch0 = MiqroChannel() + self.submodules.ch1 = MiqroChannel() + + self.submodules.serializer = SerDes( + n_data=8, t_clk=8, d_clk=0b00001111, + n_frame=10, n_crc=6, poly=0x2f) + self.submodules.intf = SerInterface(pins, pins_n) + self.comb += [ + Cat(self.intf.data[:-1]).eq(Cat(self.serializer.data[:-1])), + self.serializer.data[-1].eq(self.intf.data[-1]), + ] + + header = Record([ + ("we", 1), + ("addr", 7), + ("data", 8), + ("type", 4) + ]) + self.comb += [ + self.serializer.payload.eq(Cat( + header.raw_bits(), + self.ch0.pulse.payload, + self.ch1.pulse.payload, + )), + self.ch0.ack.eq(self.serializer.stb), + self.ch1.ack.eq(self.serializer.stb), + ] + + re_dly = Signal(3) # stage, send, respond + self.sync.rtio += [ + header.type.eq(1), # body type is miqro pulse data + If(self.serializer.stb, + header.we.eq(0), + re_dly.eq(re_dly[1:]), + ), + If(self.rtlink.o.stb, + re_dly[-1].eq(~self.rtlink.o.address[-1]), + header.we.eq(self.rtlink.o.address[-1]), + header.addr.eq(self.rtlink.o.address), + header.data.eq(self.rtlink.o.data), + ), + self.rtlink.i.stb.eq(re_dly[0] & self.serializer.stb), + self.rtlink.i.data.eq(self.serializer.readback), + ] From 31663556b8777d8d669148eaae79b9f26208fe41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 2 Sep 2022 09:32:02 +0000 Subject: [PATCH 07/72] phaser: add miqro mode --- artiq/gateware/eem.py | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/artiq/gateware/eem.py b/artiq/gateware/eem.py index 467f3cae2..eaa2f4fb2 100644 --- a/artiq/gateware/eem.py +++ b/artiq/gateware/eem.py @@ -709,20 +709,33 @@ class Phaser(_EEM): ) for pol in "pn"] @classmethod - def add_std(cls, target, eem, iostandard=default_iostandard): + def add_std(cls, target, eem, mode, iostandard=default_iostandard): cls.add_extension(target, eem, iostandard=iostandard) - phy = phaser.Phaser( - target.platform.request("phaser{}_ser_p".format(eem)), - target.platform.request("phaser{}_ser_n".format(eem))) - target.submodules += phy - target.rtio_channels.extend([ - rtio.Channel.from_phy(phy, ififo_depth=4), - rtio.Channel.from_phy(phy.ch0.frequency), - rtio.Channel.from_phy(phy.ch0.phase_amplitude), - rtio.Channel.from_phy(phy.ch1.frequency), - rtio.Channel.from_phy(phy.ch1.phase_amplitude), - ]) + if mode == "phaser": + phy = phaser.Phaser( + target.platform.request("phaser{}_ser_p".format(eem)), + target.platform.request("phaser{}_ser_n".format(eem))) + target.submodules += phy + target.rtio_channels.extend([ + rtio.Channel.from_phy(phy, ififo_depth=4), + rtio.Channel.from_phy(phy.ch0.frequency), + rtio.Channel.from_phy(phy.ch0.phase_amplitude), + rtio.Channel.from_phy(phy.ch1.frequency), + rtio.Channel.from_phy(phy.ch1.phase_amplitude), + ]) + elif mode == "miqro": + phy = phaser.Miqro( + target.platform.request("phaser{}_ser_p".format(eem)), + target.platform.request("phaser{}_ser_n".format(eem))) + target.submodules += phy + target.rtio_channels.extend([ + rtio.Channel.from_phy(phy, ififo_depth=4), + rtio.Channel.from_phy(phy.ch0), + rtio.Channel.from_phy(phy.ch1), + ]) + else: + raise ValueError("invalid mode", mode) class HVAmp(_EEM): From a20087848d4e90080670cf1a46611ad1baa492f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 2 Sep 2022 11:03:23 +0000 Subject: [PATCH 08/72] differentiate phaser modes --- artiq/coredevice/phaser.py | 65 ++++++++++++++++------------ artiq/frontend/artiq_ddb_template.py | 6 ++- artiq/gateware/eem.py | 6 +-- artiq/gateware/rtio/phy/phaser.py | 2 +- 4 files changed, 47 insertions(+), 32 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 9fad1d964..5c8f64668 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -190,7 +190,7 @@ class Phaser: def __init__(self, dmgr, channel_base, miso_delay=1, tune_fifo_offset=True, clk_sel=0, sync_dly=0, dac=None, trf0=None, trf1=None, - core_device="core"): + mode="base", core_device="core"): self.channel_base = channel_base self.core = dmgr.get(core_device) # TODO: auto-align miso-delay in phy @@ -230,6 +230,8 @@ class Phaser: if debug: print("gw_rev:", gw_rev) self.core.break_realtime() + is_base = gw_rev == 1 + is_miqro = gw_rev == 2 delay(.1*ms) # slack # allow a few errors during startup and alignment since boot @@ -350,32 +352,33 @@ class Phaser: channel.set_servo(profile=0, enable=0, hold=1) - # test oscillators and DUC - for i in range(len(channel.oscillator)): - oscillator = channel.oscillator[i] - asf = 0 - if i == 0: - asf = 0x7fff - # 6pi/4 phase - oscillator.set_amplitude_phase_mu(asf=asf, pow=0xc000, clr=1) + if is_base: + # test oscillators and DUC + for i in range(len(channel.oscillator)): + oscillator = channel.oscillator[i] + asf = 0 + if i == 0: + asf = 0x7fff + # 6pi/4 phase + oscillator.set_amplitude_phase_mu(asf=asf, pow=0xc000, clr=1) + delay(1*us) + # 3pi/4 + channel.set_duc_phase_mu(0x6000) + channel.set_duc_cfg(select=0, clr=1) + self.duc_stb() + delay(.1*ms) # settle link, pipeline and impulse response + data = channel.get_dac_data() delay(1*us) - # 3pi/4 - channel.set_duc_phase_mu(0x6000) - channel.set_duc_cfg(select=0, clr=1) - self.duc_stb() - delay(.1*ms) # settle link, pipeline and impulse response - data = channel.get_dac_data() - delay(1*us) - channel.oscillator[0].set_amplitude_phase_mu(asf=0, pow=0xc000, - clr=1) - delay(.1*ms) - sqrt2 = 0x5a81 # 0x7fff/sqrt(2) - data_i = data & 0xffff - data_q = (data >> 16) & 0xffff - # allow ripple - if (data_i < sqrt2 - 30 or data_i > sqrt2 or - abs(data_i - data_q) > 2): - raise ValueError("DUC+oscillator phase/amplitude test failed") + channel.oscillator[0].set_amplitude_phase_mu(asf=0, pow=0xc000, + clr=1) + delay(.1*ms) + sqrt2 = 0x5a81 # 0x7fff/sqrt(2) + data_i = data & 0xffff + data_q = (data >> 16) & 0xffff + # allow ripple + if (data_i < sqrt2 - 30 or data_i > sqrt2 or + abs(data_i - data_q) > 2): + raise ValueError("DUC+oscillator phase/amplitude test failed") if is_baseband: continue @@ -826,6 +829,7 @@ class PhaserChannel: self.trf_mmap = TRF372017(trf).get_mmap() self.oscillator = [PhaserOscillator(self, osc) for osc in range(5)] + self.miqro = Miqro(self) @kernel def get_dac_data(self) -> TInt32: @@ -1139,7 +1143,7 @@ class PhaserChannel: for data in [b0, b1, a1, offset]: self.phaser.write16(addr, data) addr += 2 - + @kernel def set_iir(self, profile, kp, ki=0., g=0., x_offset=0., y_offset=0.): """Set servo profile IIR coefficients. @@ -1269,3 +1273,10 @@ class PhaserOscillator: raise ValueError("amplitude out of bounds") pow = int32(round(phase*(1 << 16))) self.set_amplitude_phase_mu(asf, pow, clr) + + +class Miqro: + def __init__(self, channel): + self.channel = channel + self.base_addr = (self.channel.phaser.channel_base + 1 + + self.channel.index) << 8 diff --git a/artiq/frontend/artiq_ddb_template.py b/artiq/frontend/artiq_ddb_template.py index b6d9294a3..503f4862e 100755 --- a/artiq/frontend/artiq_ddb_template.py +++ b/artiq/frontend/artiq_ddb_template.py @@ -559,6 +559,7 @@ class PeripheralManager: return 1 def process_phaser(self, rtio_offset, peripheral): + mode = peripheral.get("mode", "base") self.gen(""" device_db["{name}"] = {{ "type": "local", @@ -567,11 +568,14 @@ class PeripheralManager: "arguments": {{ "channel_base": 0x{channel:06x}, "miso_delay": 1, + "mode": "{mode}" }} }}""", name=self.get_name("phaser"), + mode=mode, channel=rtio_offset) - return 5 + rtio_channels = {"base": 5, "miqro": 3}[mode] + return rtio_channels def process_hvamp(self, rtio_offset, peripheral): hvamp_name = self.get_name("hvamp") diff --git a/artiq/gateware/eem.py b/artiq/gateware/eem.py index eaa2f4fb2..3cc55e3f0 100644 --- a/artiq/gateware/eem.py +++ b/artiq/gateware/eem.py @@ -709,11 +709,11 @@ class Phaser(_EEM): ) for pol in "pn"] @classmethod - def add_std(cls, target, eem, mode, iostandard=default_iostandard): + def add_std(cls, target, eem, mode="base", iostandard=default_iostandard): cls.add_extension(target, eem, iostandard=iostandard) - if mode == "phaser": - phy = phaser.Phaser( + if mode == "base": + phy = phaser.Base( target.platform.request("phaser{}_ser_p".format(eem)), target.platform.request("phaser{}_ser_n".format(eem))) target.submodules += phy diff --git a/artiq/gateware/rtio/phy/phaser.py b/artiq/gateware/rtio/phy/phaser.py index 038f8b2c5..483e0945c 100644 --- a/artiq/gateware/rtio/phy/phaser.py +++ b/artiq/gateware/rtio/phy/phaser.py @@ -28,7 +28,7 @@ class DDSChannel(Module): [Cat(i.a, i.clr, i.p) for i in self.dds.i]) -class Phaser(Module): +class Base(Module): def __init__(self, pins, pins_n): self.rtlink = rtlink.Interface( rtlink.OInterface(data_width=8, address_width=8, From cf48232a90c87b7282ef1234693efc6b04dd54ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 2 Sep 2022 14:38:38 +0000 Subject: [PATCH 09/72] fixes --- artiq/coredevice/phaser.py | 59 +++++++++++++++++++++++++++++++ artiq/gateware/eem_7series.py | 3 +- artiq/gateware/rtio/phy/phaser.py | 8 ++--- 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 5c8f64668..53696752e 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -47,6 +47,9 @@ PHASER_ADDR_SERVO_CFG1 = 0x31 # 0x32 - 0x71 servo coefficients + offset data PHASER_ADDR_SERVO_DATA_BASE = 0x32 +# 0x78 Miqro channel profile/window memories +PHASER_ADDR_MIQRO_ADDR = 0x78 +PHASER_ADDR_MIQRO_DATA = 0x7a PHASER_SEL_DAC = 1 << 0 PHASER_SEL_TRF0 = 1 << 1 @@ -1280,3 +1283,59 @@ class Miqro: self.channel = channel self.base_addr = (self.channel.phaser.channel_base + 1 + self.channel.index) << 8 + + @kernel + def write8(self, addr, data): + self.channel.phaser.write16(PHASER_ADDR_MIQRO_ADDR, + (self.channel.index << 13) | addr) + self.channel.phaser.write8(PHASER_ADDR_MIQRO_DATA, + data) + + @kernel + def write32(self, addr, data): + for i in range(4): + self.write8(addr + i, data >> (i * 8)) + + @kernel + def set_frequency_mu(self, oscillator, profile, ftw): + self.write32((1 << 12) | (oscillator << 8) | (profile << 3), ftw) + + @kernel + def set_amplitude_phase_mu(self, oscillator, profile, asf, pow=0): + self.write32((1 << 12) | (oscillator << 8) | (profile << 3) | (1 << 2), + (asf & 0xffff) | (pow << 16)) + + @kernel + def set_window(self, start, data, rate=1, shift=0, order=3, head=1, tail=1): + if len(data) == 0 or len(data) >= (1 << 10): + raise ValueError("invalid window length") + if rate < 1 or rate > 1 << 12: + raise ValueError("rate out of bounds") + addr = start << 2 + self.write32(addr, + ((start + 1 + len(data)) & 0x3ff) + | ((rate - 1) << 10) + | (shift << 22) + | (order << 28) + | (head << 30) + | (tail << 31) + ) + for i in range(len(data)): + addr += 4 + self.write32(addr, (data[i][0] & 0xffff) | (data[i][1] << 16)) + + @kernel + def pulse(self, window, profiles): + data = [window, 0, 0] + word = 0 + idx = 10 + for i in range(len(profiles)): + if idx >= 30: + word += 1 + idx = 0 + data[word] |= profiles[i] << (idx * 5) + idx += 5 + while word >= 0: + rtio_output(self.base_addr + word, data[word]) + delay_mu(8) + word -= 1 diff --git a/artiq/gateware/eem_7series.py b/artiq/gateware/eem_7series.py index 1983b1009..2cd258ece 100644 --- a/artiq/gateware/eem_7series.py +++ b/artiq/gateware/eem_7series.py @@ -123,7 +123,8 @@ def peripheral_fastino(module, peripheral, **kwargs): def peripheral_phaser(module, peripheral, **kwargs): if len(peripheral["ports"]) != 1: raise ValueError("wrong number of ports") - eem.Phaser.add_std(module, peripheral["ports"][0], **kwargs) + eem.Phaser.add_std(module, peripheral["ports"][0], + peripheral.get("mode", "base"), **kwargs) def peripheral_hvamp(module, peripheral, **kwargs): diff --git a/artiq/gateware/rtio/phy/phaser.py b/artiq/gateware/rtio/phy/phaser.py index 483e0945c..455d93cef 100644 --- a/artiq/gateware/rtio/phy/phaser.py +++ b/artiq/gateware/rtio/phy/phaser.py @@ -91,7 +91,7 @@ class Base(Module): class MiqroChannel(Module): - def __init__(self, regs): + def __init__(self): self.rtlink = rtlink.Interface( rtlink.OInterface(data_width=30, address_width=2, fine_ts_width=1, enable_replace=False)) @@ -100,7 +100,7 @@ class MiqroChannel(Module): regs = [Signal(30, reset_less=True) for _ in range(3)] dt = Signal(7, reset_less=True) stb = Signal() - self.comb += self.pulase.payload.data.eq(Cat(stb, dt, regs)) + self.comb += self.pulse.eq(Cat(stb, dt, regs)) self.sync.rtio += [ dt.eq(dt + 2), If(self.ack, @@ -145,8 +145,8 @@ class Miqro(Module): self.comb += [ self.serializer.payload.eq(Cat( header.raw_bits(), - self.ch0.pulse.payload, - self.ch1.pulse.payload, + self.ch0.pulse, + self.ch1.pulse, )), self.ch0.ack.eq(self.serializer.stb), self.ch1.ack.eq(self.serializer.stb), From 25c0dc468817931e3e8b8936375a1a50688b6f40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 2 Sep 2022 14:54:18 +0000 Subject: [PATCH 10/72] whitespace --- artiq/gateware/eem_7series.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artiq/gateware/eem_7series.py b/artiq/gateware/eem_7series.py index 2cd258ece..e5980b980 100644 --- a/artiq/gateware/eem_7series.py +++ b/artiq/gateware/eem_7series.py @@ -130,7 +130,7 @@ def peripheral_phaser(module, peripheral, **kwargs): def peripheral_hvamp(module, peripheral, **kwargs): if len(peripheral["ports"]) != 1: raise ValueError("wrong number of ports") - eem.HVAmp.add_std(module, peripheral["ports"][0], + eem.HVAmp.add_std(module, peripheral["ports"][0], ttl_simple.Output, **kwargs) From 0df2cadcd3e398cb313c06b72d0ff840ee0dd47f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 2 Sep 2022 15:29:36 +0000 Subject: [PATCH 11/72] fixes --- artiq/coredevice/phaser.py | 29 ++++++++++++++++++++--------- artiq/gateware/rtio/phy/phaser.py | 9 +++++++-- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 53696752e..2cc8832ea 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -1288,8 +1288,7 @@ class Miqro: def write8(self, addr, data): self.channel.phaser.write16(PHASER_ADDR_MIQRO_ADDR, (self.channel.index << 13) | addr) - self.channel.phaser.write8(PHASER_ADDR_MIQRO_DATA, - data) + self.channel.phaser.write8(PHASER_ADDR_MIQRO_DATA, data) @kernel def write32(self, addr, data): @@ -1298,16 +1297,24 @@ class Miqro: @kernel def set_frequency_mu(self, oscillator, profile, ftw): + if oscillator >= 16: + raise ValueError("invalid oscillator index") + if profile >= 32: + raise ValueError("invalid profile index") self.write32((1 << 12) | (oscillator << 8) | (profile << 3), ftw) @kernel def set_amplitude_phase_mu(self, oscillator, profile, asf, pow=0): + if oscillator >= 16: + raise ValueError("invalid oscillator index") + if profile >= 32: + raise ValueError("invalid profile index") self.write32((1 << 12) | (oscillator << 8) | (profile << 3) | (1 << 2), (asf & 0xffff) | (pow << 16)) @kernel - def set_window(self, start, data, rate=1, shift=0, order=3, head=1, tail=1): - if len(data) == 0 or len(data) >= (1 << 10): + def set_window(self, start, data, rate=1, shift=0, order=0, head=1, tail=1): + if len(data) >= 1 << 10: raise ValueError("invalid window length") if rate < 1 or rate > 1 << 12: raise ValueError("rate out of bounds") @@ -1315,10 +1322,10 @@ class Miqro: self.write32(addr, ((start + 1 + len(data)) & 0x3ff) | ((rate - 1) << 10) - | (shift << 22) - | (order << 28) - | (head << 30) - | (tail << 31) + | ((shift & 0x3f) << 22) + | ((order & 3) << 28) + | ((head & 1) << 30) + | ((tail & 1) << 31) ) for i in range(len(data)): addr += 4 @@ -1326,6 +1333,10 @@ class Miqro: @kernel def pulse(self, window, profiles): + if len(profiles) > 16: + raise ValueError("too many oscillators") + if window > 0x3ff: + raise ValueError("invalid window") data = [window, 0, 0] word = 0 idx = 10 @@ -1333,7 +1344,7 @@ class Miqro: if idx >= 30: word += 1 idx = 0 - data[word] |= profiles[i] << (idx * 5) + data[word] |= (profiles[i] & 0x1f) << idx idx += 5 while word >= 0: rtio_output(self.base_addr + word, data[word]) diff --git a/artiq/gateware/rtio/phy/phaser.py b/artiq/gateware/rtio/phy/phaser.py index 455d93cef..078984477 100644 --- a/artiq/gateware/rtio/phy/phaser.py +++ b/artiq/gateware/rtio/phy/phaser.py @@ -100,11 +100,16 @@ class MiqroChannel(Module): regs = [Signal(30, reset_less=True) for _ in range(3)] dt = Signal(7, reset_less=True) stb = Signal() - self.comb += self.pulse.eq(Cat(stb, dt, regs)) + pulse = Cat(stb, dt, regs) + assert len(self.pulse) >= len(pulse) + self.comb += [ + self.pulse.eq(pulse), + self.rtlink.o.busy.eq(stb & ~self.ack), + ] self.sync.rtio += [ dt.eq(dt + 2), If(self.ack, - dt.eq(0), + dt[1:].eq(0), stb.eq(0), ), If(self.rtlink.o.stb, From d6d0c2c866372e6b644f8fe6439965ebfee93a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 2 Sep 2022 15:55:28 +0000 Subject: [PATCH 12/72] miqro: name register constants --- artiq/coredevice/phaser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 2cc8832ea..0d0ab7141 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -48,8 +48,8 @@ PHASER_ADDR_SERVO_CFG1 = 0x31 PHASER_ADDR_SERVO_DATA_BASE = 0x32 # 0x78 Miqro channel profile/window memories -PHASER_ADDR_MIQRO_ADDR = 0x78 -PHASER_ADDR_MIQRO_DATA = 0x7a +PHASER_ADDR_MIQRO_MEM_ADDR = 0x78 +PHASER_ADDR_MIQRO_MEM_DATA = 0x7a PHASER_SEL_DAC = 1 << 0 PHASER_SEL_TRF0 = 1 << 1 @@ -1286,9 +1286,9 @@ class Miqro: @kernel def write8(self, addr, data): - self.channel.phaser.write16(PHASER_ADDR_MIQRO_ADDR, + self.channel.phaser.write16(PHASER_ADDR_MIQRO_MEM_ADDR, (self.channel.index << 13) | addr) - self.channel.phaser.write8(PHASER_ADDR_MIQRO_DATA, data) + self.channel.phaser.write8(PHASER_ADDR_MIQRO_MEM_DATA, data) @kernel def write32(self, addr, data): From b9727fdfce17a56e8b4c4cb372b38f704a216d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 2 Sep 2022 16:38:53 +0000 Subject: [PATCH 13/72] refactor for 32 bit mem access --- artiq/coredevice/phaser.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 0d0ab7141..b53782a3a 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -48,8 +48,8 @@ PHASER_ADDR_SERVO_CFG1 = 0x31 PHASER_ADDR_SERVO_DATA_BASE = 0x32 # 0x78 Miqro channel profile/window memories -PHASER_ADDR_MIQRO_MEM_ADDR = 0x78 -PHASER_ADDR_MIQRO_MEM_DATA = 0x7a +PHASER_ADDR_MIQRO_MEM_ADDR = 0x72 +PHASER_ADDR_MIQRO_MEM_DATA = 0x74 PHASER_SEL_DAC = 1 << 0 PHASER_SEL_TRF0 = 1 << 1 @@ -1285,15 +1285,16 @@ class Miqro: self.channel.index) << 8 @kernel - def write8(self, addr, data): + def write32(self, addr, data): self.channel.phaser.write16(PHASER_ADDR_MIQRO_MEM_ADDR, - (self.channel.index << 13) | addr) - self.channel.phaser.write8(PHASER_ADDR_MIQRO_MEM_DATA, data) + (self.channel.index << 15) | addr) + self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, data) @kernel - def write32(self, addr, data): - for i in range(4): - self.write8(addr + i, data >> (i * 8)) + def read32(self, addr): + self.channel.phaser.write16(PHASER_ADDR_MIQRO_MEM_ADDR, + (self.channel.index << 15) | addr) + return self.channel.phaser.read32(PHASER_ADDR_MIQRO_MEM_DATA) @kernel def set_frequency_mu(self, oscillator, profile, ftw): @@ -1301,7 +1302,7 @@ class Miqro: raise ValueError("invalid oscillator index") if profile >= 32: raise ValueError("invalid profile index") - self.write32((1 << 12) | (oscillator << 8) | (profile << 3), ftw) + self.write32((1 << 14) | (oscillator << 6) | (profile << 1), ftw) @kernel def set_amplitude_phase_mu(self, oscillator, profile, asf, pow=0): @@ -1309,26 +1310,30 @@ class Miqro: raise ValueError("invalid oscillator index") if profile >= 32: raise ValueError("invalid profile index") - self.write32((1 << 12) | (oscillator << 8) | (profile << 3) | (1 << 2), + self.write32((1 << 14) | (oscillator << 6) | (profile << 1) | 1, (asf & 0xffff) | (pow << 16)) @kernel - def set_window(self, start, data, rate=1, shift=0, order=0, head=1, tail=1): + def set_window(self, start, data, rate=1, shift=0, order=0, head=0, tail=0): if len(data) >= 1 << 10: raise ValueError("invalid window length") if rate < 1 or rate > 1 << 12: raise ValueError("rate out of bounds") - addr = start << 2 + if shift > 0x3f: + raise ValueError("shift out of bounds") + if order > 3: + raise ValueError("order out of bounds") + addr = start self.write32(addr, ((start + 1 + len(data)) & 0x3ff) | ((rate - 1) << 10) - | ((shift & 0x3f) << 22) - | ((order & 3) << 28) + | (shift << 22) + | (order << 28) | ((head & 1) << 30) | ((tail & 1) << 31) ) for i in range(len(data)): - addr += 4 + addr += 1 self.write32(addr, (data[i][0] & 0xffff) | (data[i][1] << 16)) @kernel From 3809ac5470961092cfbd8d28250ef38b9c8462d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 2 Sep 2022 19:47:06 +0000 Subject: [PATCH 14/72] fix type, clean clear --- artiq/coredevice/phaser.py | 1 + artiq/gateware/rtio/phy/phaser.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index b53782a3a..386c1897a 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -1351,6 +1351,7 @@ class Miqro: idx = 0 data[word] |= (profiles[i] & 0x1f) << idx idx += 5 + delay_mu(-8*word) while word >= 0: rtio_output(self.base_addr + word, data[word]) delay_mu(8) diff --git a/artiq/gateware/rtio/phy/phaser.py b/artiq/gateware/rtio/phy/phaser.py index 078984477..1a1ced03d 100644 --- a/artiq/gateware/rtio/phy/phaser.py +++ b/artiq/gateware/rtio/phy/phaser.py @@ -111,6 +111,9 @@ class MiqroChannel(Module): If(self.ack, dt[1:].eq(0), stb.eq(0), + If(stb, + [r.eq(0) for r in regs], + ), ), If(self.rtlink.o.stb, Array(regs)[self.rtlink.o.address].eq(self.rtlink.o.data), @@ -159,7 +162,7 @@ class Miqro(Module): re_dly = Signal(3) # stage, send, respond self.sync.rtio += [ - header.type.eq(1), # body type is miqro pulse data + header.type.eq(3), # body type is miqro pulse data If(self.serializer.stb, header.we.eq(0), re_dly.eq(re_dly[1:]), From b6586cd7e4bd4e7825d99502c76c4b813fefcd24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 2 Sep 2022 20:45:13 +0000 Subject: [PATCH 15/72] add window data delay --- artiq/coredevice/phaser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 386c1897a..aa8c36819 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -1335,6 +1335,7 @@ class Miqro: for i in range(len(data)): addr += 1 self.write32(addr, (data[i][0] & 0xffff) | (data[i][1] << 16)) + delay(10*us) @kernel def pulse(self, window, profiles): From f4d325112ce3d9db41d814650ee0254ad41545ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Sun, 4 Sep 2022 11:19:38 +0000 Subject: [PATCH 16/72] reset and elaborate, si functions --- artiq/coredevice/phaser.py | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index aa8c36819..f8ac004d9 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -1296,6 +1296,21 @@ class Miqro: (self.channel.index << 15) | addr) return self.channel.phaser.read32(PHASER_ADDR_MIQRO_MEM_DATA) + @kernel + def reset(self): + for osc in range(16): + for pro in range(32): + self.set_frequency_mu(osc, pro, 0) + self.set_amplitude_phase_mu(osc, pro, 0, 0) + delay(20*us) + self.set_window_mu( + start=0, data=[[0, 0]], rate=1, shift=0, order=0, head=0, tail=1) + # naive check that we are actually writing something + if self.read32(0) != (1 << 31) | 2 or self.read32(1) != 0: + raise ValueError("window write failed") + delay(100*us) + self.pulse(window=0, profiles=[0]) + @kernel def set_frequency_mu(self, oscillator, profile, ftw): if oscillator >= 16: @@ -1304,6 +1319,11 @@ class Miqro: raise ValueError("invalid profile index") self.write32((1 << 14) | (oscillator << 6) | (profile << 1), ftw) + @kernel + def set_frequency(oscillator, profile, frequency): + ftw = int32(round(frequency*((1 << 30)/(62.5*MHz)))) + self.set_frequency_mu(ftw) + @kernel def set_amplitude_phase_mu(self, oscillator, profile, asf, pow=0): if oscillator >= 16: @@ -1314,9 +1334,19 @@ class Miqro: (asf & 0xffff) | (pow << 16)) @kernel - def set_window(self, start, data, rate=1, shift=0, order=0, head=0, tail=0): + def set_amplitude_phase(self, oscillator, profile, amplitude, phase=0): + asf = int32(round(amplitude*0xffff)) + if asf < 0 or asf > 0xffff: + raise ValueError("amplitude out of bounds") + pow = int32(round(phase*(1 << 16))) + self.set_amplitude_phase_mu(oscillator, profile, asf, pow) + + @kernel + def set_window_mu(self, start, data, rate=1, shift=0, order=0, head=0, tail=0): + if start >= 1 << 10: + raise ValueError("start out of bouncs") if len(data) >= 1 << 10: - raise ValueError("invalid window length") + raise ValueError("window length out of bounds") if rate < 1 or rate > 1 << 12: raise ValueError("rate out of bounds") if shift > 0x3f: @@ -1342,11 +1372,13 @@ class Miqro: if len(profiles) > 16: raise ValueError("too many oscillators") if window > 0x3ff: - raise ValueError("invalid window") + raise ValueError("window start out of bounds") data = [window, 0, 0] word = 0 idx = 10 for i in range(len(profiles)): + if profiles[i] > 0x1f: + raise ValueError("profile out of bounds") if idx >= 30: word += 1 idx = 0 From fa3678f8a380c61d0d368be3ffb1eb78de8f00ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Sun, 4 Sep 2022 12:03:44 +0000 Subject: [PATCH 17/72] mem auto increment --- artiq/coredevice/phaser.py | 52 +++++++++++++------------------------- 1 file changed, 18 insertions(+), 34 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index f8ac004d9..ca130c59c 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -1290,56 +1290,38 @@ class Miqro: (self.channel.index << 15) | addr) self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, data) - @kernel - def read32(self, addr): - self.channel.phaser.write16(PHASER_ADDR_MIQRO_MEM_ADDR, - (self.channel.index << 15) | addr) - return self.channel.phaser.read32(PHASER_ADDR_MIQRO_MEM_DATA) - @kernel def reset(self): for osc in range(16): for pro in range(32): - self.set_frequency_mu(osc, pro, 0) - self.set_amplitude_phase_mu(osc, pro, 0, 0) + self.set_profile_mu(osc, pro, 0, 0, 0) delay(20*us) self.set_window_mu( start=0, data=[[0, 0]], rate=1, shift=0, order=0, head=0, tail=1) - # naive check that we are actually writing something - if self.read32(0) != (1 << 31) | 2 or self.read32(1) != 0: - raise ValueError("window write failed") - delay(100*us) self.pulse(window=0, profiles=[0]) @kernel - def set_frequency_mu(self, oscillator, profile, ftw): + def set_profile_mu(self, oscillator, profile, ftw, asf, pow=0): if oscillator >= 16: raise ValueError("invalid oscillator index") if profile >= 32: raise ValueError("invalid profile index") - self.write32((1 << 14) | (oscillator << 6) | (profile << 1), ftw) - - @kernel - def set_frequency(oscillator, profile, frequency): - ftw = int32(round(frequency*((1 << 30)/(62.5*MHz)))) - self.set_frequency_mu(ftw) - - @kernel - def set_amplitude_phase_mu(self, oscillator, profile, asf, pow=0): - if oscillator >= 16: - raise ValueError("invalid oscillator index") - if profile >= 32: - raise ValueError("invalid profile index") - self.write32((1 << 14) | (oscillator << 6) | (profile << 1) | 1, + self.channel.phaser.write16(PHASER_ADDR_MIQRO_MEM_ADDR, + (self.channel.index << 15) | (1 << 14) | (oscillator << 6) | + (profile << 1)) + self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, ftw) + self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, (asf & 0xffff) | (pow << 16)) @kernel - def set_amplitude_phase(self, oscillator, profile, amplitude, phase=0): + def set_profile(oscillator, profile, frequency, amplitude, phase=0.): + # frequency is interpreted in the Nyquist sense, i.e. aliased + ftw = int32(round(frequency*((1 << 30)/(62.5*MHz)))) asf = int32(round(amplitude*0xffff)) if asf < 0 or asf > 0xffff: raise ValueError("amplitude out of bounds") pow = int32(round(phase*(1 << 16))) - self.set_amplitude_phase_mu(oscillator, profile, asf, pow) + self.set_profile_mu(oscillator, profile, ftw, asf, pow) @kernel def set_window_mu(self, start, data, rate=1, shift=0, order=0, head=0, tail=0): @@ -1353,8 +1335,9 @@ class Miqro: raise ValueError("shift out of bounds") if order > 3: raise ValueError("order out of bounds") - addr = start - self.write32(addr, + self.channel.phaser.write16(PHASER_ADDR_MIQRO_MEM_ADDR, + (self.channel.index << 15) | start) + self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, ((start + 1 + len(data)) & 0x3ff) | ((rate - 1) << 10) | (shift << 22) @@ -1362,10 +1345,11 @@ class Miqro: | ((head & 1) << 30) | ((tail & 1) << 31) ) - for i in range(len(data)): - addr += 1 - self.write32(addr, (data[i][0] & 0xffff) | (data[i][1] << 16)) + for d in data: + self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, + (d[0] & 0xffff) | (d[1] << 16)) delay(10*us) + return (start + 1 + len(data)) & 0x3ff @kernel def pulse(self, window, profiles): From 876f26ee30a10c59fe15189206f691d5b301492f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Sun, 4 Sep 2022 19:21:49 +0000 Subject: [PATCH 18/72] add some docs --- artiq/coredevice/phaser.py | 106 ++++++++++++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index ca130c59c..7686bf40c 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -1279,6 +1279,110 @@ class PhaserOscillator: class Miqro: + """ + Miqro pulse generator. + + Notes + ----- + * The `_mu` suffix in method names refers to parameters and data in "machine units", + i.e. integers. Conversion methods and wrappers to convert from SI units (Hz frequency, + full scale amplitude, turns phase, seconds time) are provided. + * The annotation that some operation is "expensive" does not mean it is impossible, just + that it may take a significant amount of time and resources to execute such that + it may be impractical when used often or during fast pulse sequences. + They are intended for use in calibration and initialization. + + Functionality + ------------- + A Miqro instance represents one RF output. + The output is generated by with the following data flow: + + ### Oscillators + * There are n_osc = 16 oscillators with oscillator IDs 0..n_osc-1. + * Each oscillator outputs one tone at any given time + I/Q (quadrature, a.k.a. complex) 2x16 bit signed data + at tau = 4 ns sample intervals, 250 MS/s, Nyquist 125 MHz, bandwidth 200 MHz + (from f = -100..+100 MHz, taking into account the interpolation anti-aliasing + filters in subsequent interpolators), + 32 bit frequency (f) resolution (~ 1/16 Hz), + 16 bit unsigned amplitude (a) resolution + 16 bit phase (p) resolution + * The output phase p' of each oscillator at time t (boot/reset/initialization of the + device at t=0) is then p' = f*t + p (mod 1 turn) where f and p are the + (currently active) profile frequency and phase. + The terms "phase coherent" and "phase tracking" are defined to refer to this + choice of oscillator output phase p'. + Note that the phase p is not accumulated (on top of previous + phases, previous profiles, or oscillator history). It is "absolute" in the + sense that frequency f and phase p fully determine oscillator + output phase p' at time t. This is unlike typical DDS behavior. + * Frequency, phase and amplitude of each oscillator are configurable by selecting one of + n_profile = 32 profiles 0..n_profile-1. This selection is fast and can be done for + each pulse. + * Note: one profile per oscillator (usually profile index 0) should be reserved + for the NOP (no operation, identity) profile, usually with zero + amplitude. + * Data for each profile for each oscillator can be configured + individually. Storing profile data should be considered "expensive". + + ### Summation + * The oscillator outputs are added together (wrapping addition). + * The user must ensure that the sum of oscillators outputs does + not exceed the (16 bit signed) data range. In general that means that the sum of the + amplitudes must not exceed the range. + + ### Shaper + * The summed output stream is then multiplied with a the complex-valued output of a + triggerable shaper. + * Triggering the shaper corresponds to passing a pulse from all + oscillators to the RF output. + * Any previously staged profiles and phase offsets become active simultaneously + (on the same output sample) when triggering the shaper. + * The shaper reads (replays) window samples from a memory of size n_window = 1 << 10 starting + and stopping at memory locations specified. + * Each window memory segment starts with a header determining segment + length and interpolation parameters. + * The window samples are interpolated by a factor (rate change) r where log2(r) = 0..n_cic=12 + selectable when triggering. + * The interpolation order is constant, linear, quadratic, or cubic. This + corresponds to interpolation modes from rectangular window (1st order CIC) + or zero order hold) and to Parzen window (4th order CIC, cubic spline), + selectable when triggering. + * This results in support for pulse lengths of between tau and a bit more than + (1 << 12 + 10) tau ~ 17 ms. + * Windows can be configured to be head-less and/or tail-less, meaning, they + do not feed zero-amplitude samples into the shaper before and after + each window. This is used to implement pulses with arbitrary length or + CW output. + * The window memory can be segmented by choosing different start indices + to support different windows selectable when triggering. + + ### DAC + * This section of the data flow is analogous to the `base` Phaser mode. + * The DAC receives the 250 MS/s I/Q data stream and interpolates it to 1 GS/s I/Q + (with a bandwidth 200 MHz). + * It then applies a (expensive to change) frequency offset of + f1 = -400 MHz..400 MHz. + * Then the DAC converts the data stream to 2 analog outputs I and Q. + * The signals go through two anti-aliasing filters with 340 MHz 3dB bandwidth. + + ### IQ Mixer and PLL (Upconverter variant) + * The analog I and Q signals after the filter are upconverted in a single-sideband IQ + mixer with a f2 = 0.3 GHz..4.8 GHz LO (the "carrier"). + * The output goes through a digitally switchable attenuator (0..31.5 dB attenuation) and + is available at an SMA output with a typical max signal level of 0 to -10 dBm (TBC). + + ### Overall properties + * The resulting phase of that signal is + (f + f1 + f2)*t + p + s(t - t0) + p1 + p2 (mod 1 turn) + where p1 and p2 are constant but arbitrary and undetermined phase offsets of the + two (common) upconversion stages, s(t - t0) is the phase of the interpolated + shaper output, and t0 is the trigger time (fiducial of the shaper). + Unsurprisingly the frequency is the derivative of the phase. + * The minimum time to change profiles and phase offsets is ~128 ns (estimate, TBC). + This is the minimum practical pulse interval. + """ + def __init__(self, channel): self.channel = channel self.base_addr = (self.channel.phaser.channel_base + 1 + @@ -1314,7 +1418,7 @@ class Miqro: (asf & 0xffff) | (pow << 16)) @kernel - def set_profile(oscillator, profile, frequency, amplitude, phase=0.): + def set_profile(self, oscillator, profile, frequency, amplitude, phase=0.): # frequency is interpreted in the Nyquist sense, i.e. aliased ftw = int32(round(frequency*((1 << 30)/(62.5*MHz)))) asf = int32(round(amplitude*0xffff)) From 263c2751b31a165bd836a93c4cc74087e8c0c7ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Sun, 4 Sep 2022 20:43:28 +0000 Subject: [PATCH 19/72] add profile_mu --- artiq/coredevice/phaser.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 7686bf40c..9bf7f042e 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -1456,12 +1456,12 @@ class Miqro: return (start + 1 + len(data)) & 0x3ff @kernel - def pulse(self, window, profiles): + def encode(self, window, profiles, data): if len(profiles) > 16: raise ValueError("too many oscillators") if window > 0x3ff: raise ValueError("window start out of bounds") - data = [window, 0, 0] + data[0] = window word = 0 idx = 10 for i in range(len(profiles)): @@ -1472,8 +1472,17 @@ class Miqro: idx = 0 data[word] |= (profiles[i] & 0x1f) << idx idx += 5 - delay_mu(-8*word) - while word >= 0: + return word + + @kernel + def pulse_mu(self, data): + for word in range(len(data) - 1, -1, -1): rtio_output(self.base_addr + word, data[word]) delay_mu(8) - word -= 1 + + @kernel + def pulse(self, window, profiles): + data = [0, 0, 0] + words = self.encode(window, profiles, data) + delay_mu(-8*words) + self.pulse_mu(data[:words]) From 1cc57e2345be79e6f07972d485ab4919695db216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Sun, 4 Sep 2022 21:00:24 +0000 Subject: [PATCH 20/72] fix len --- artiq/coredevice/phaser.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 9bf7f042e..caadc8fdc 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -383,6 +383,9 @@ class Phaser: abs(data_i - data_q) > 2): raise ValueError("DUC+oscillator phase/amplitude test failed") + if is_miqro: + channel.miqro.reset() + if is_baseband: continue @@ -1485,4 +1488,4 @@ class Miqro: data = [0, 0, 0] words = self.encode(window, profiles, data) delay_mu(-8*words) - self.pulse_mu(data[:words]) + self.pulse_mu(data[:words + 1]) From b4287ac9f450a5e62e4013efa7231dbb6a9a4718 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 5 Sep 2022 11:48:43 +0800 Subject: [PATCH 21/72] flake: add experimental feature support --- flake.nix | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index 641630975..bc2cf378e 100644 --- a/flake.nix +++ b/flake.nix @@ -134,7 +134,7 @@ doCheck = false; # FIXME }; - artiq = pkgs.python3Packages.buildPythonPackage rec { + artiq-upstream = pkgs.python3Packages.buildPythonPackage rec { pname = "artiq"; version = artiqVersion; src = self; @@ -175,6 +175,11 @@ ''; }; + artiq = artiq-upstream // { + withExperimentalFeatures = features: artiq-upstream.overrideAttrs(oa: + { patches = map (f: ./experimental-features/${f}.diff) features; }); + }; + migen = pkgs.python3Packages.buildPythonPackage rec { name = "migen"; src = src-migen; @@ -250,7 +255,7 @@ runScript = "vivado"; }; - makeArtiqBoardPackage = { target, variant, buildCommand ? "python -m artiq.gateware.targets.${target} -V ${variant}" }: + makeArtiqBoardPackage = { target, variant, buildCommand ? "python -m artiq.gateware.targets.${target} -V ${variant}", experimentalFeatures ? [] }: pkgs.stdenv.mkDerivation { name = "artiq-board-${target}-${variant}"; phases = [ "buildPhase" "checkPhase" "installPhase" ]; @@ -261,7 +266,7 @@ }; }; nativeBuildInputs = [ - (pkgs.python3.withPackages(ps: [ ps.jsonschema migen misoc artiq])) + (pkgs.python3.withPackages(ps: [ ps.jsonschema migen misoc (artiq.withExperimentalFeatures experimentalFeatures) ])) rustPlatform.rust.rustc rustPlatform.rust.cargo pkgs.llvmPackages_11.clang-unwrapped From 411afbdc23dbdded9265efe516a8a742491c50c7 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 5 Sep 2022 11:53:09 +0800 Subject: [PATCH 22/72] experimental-features: add SU Servo extension for variable number of Urukuls (PR #1782) --- experimental-features/suservo-var-urukul.diff | 771 ++++++++++++++++++ 1 file changed, 771 insertions(+) create mode 100644 experimental-features/suservo-var-urukul.diff diff --git a/experimental-features/suservo-var-urukul.diff b/experimental-features/suservo-var-urukul.diff new file mode 100644 index 000000000..e4f5a749a --- /dev/null +++ b/experimental-features/suservo-var-urukul.diff @@ -0,0 +1,771 @@ +diff --git a/artiq/coredevice/suservo.py b/artiq/coredevice/suservo.py +index 1d0a72dad..a89cdcca4 100644 +--- a/artiq/coredevice/suservo.py ++++ b/artiq/coredevice/suservo.py +@@ -3,17 +3,14 @@ + from artiq.coredevice.rtio import rtio_output, rtio_input_data + from artiq.coredevice import spi2 as spi + from artiq.coredevice import urukul, sampler ++from math import ceil, log2 + + +-COEFF_WIDTH = 18 ++COEFF_WIDTH = 18 # Must match gateware IIRWidths.coeff + Y_FULL_SCALE_MU = (1 << (COEFF_WIDTH - 1)) - 1 +-COEFF_DEPTH = 10 + 1 +-WE = 1 << COEFF_DEPTH + 1 +-STATE_SEL = 1 << COEFF_DEPTH +-CONFIG_SEL = 1 << COEFF_DEPTH - 1 +-CONFIG_ADDR = CONFIG_SEL | STATE_SEL + T_CYCLE = (2*(8 + 64) + 2)*8*ns # Must match gateware Servo.t_cycle. +-COEFF_SHIFT = 11 ++COEFF_SHIFT = 11 # Must match gateware IIRWidths.shift ++PROFILE_WIDTH = 5 # Must match gateware IIRWidths.profile + + + @portable +@@ -35,8 +32,8 @@ class SUServo: + """Sampler-Urukul Servo parent and configuration device. + + Sampler-Urukul Servo is a integrated device controlling one +- 8-channel ADC (Sampler) and two 4-channel DDS (Urukuls) with a DSP engine +- connecting the ADC data and the DDS output amplitudes to enable ++ 8-channel ADC (Sampler) and any number of 4-channel DDS (Urukuls) with a ++ DSP engine connecting the ADC data and the DDS output amplitudes to enable + feedback. SU Servo can for example be used to implement intensity + stabilization of laser beams with an amplifier and AOM driven by Urukul + and a photodetector connected to Sampler. +@@ -49,7 +46,7 @@ class SUServo: + * See the SU Servo variant of the Kasli target for an example of how to + connect the gateware and the devices. Sampler and each Urukul need + two EEM connections. +- * Ensure that both Urukuls are AD9910 variants and have the on-board ++ * Ensure that all Urukuls are AD9910 variants and have the on-board + dip switches set to 1100 (first two on, last two off). + * Refer to the Sampler and Urukul documentation and the SU Servo + example device database for runtime configuration of the devices +@@ -65,7 +62,8 @@ class SUServo: + :param core_device: Core device name + """ + kernel_invariants = {"channel", "core", "pgia", "cplds", "ddses", +- "ref_period_mu"} ++ "ref_period_mu", "num_channels", "coeff_sel", ++ "state_sel", "config_addr", "write_enable"} + + def __init__(self, dmgr, channel, pgia_device, + cpld_devices, dds_devices, +@@ -83,9 +81,19 @@ def __init__(self, dmgr, channel, pgia_device, + self.core.coarse_ref_period) + assert self.ref_period_mu == self.core.ref_multiplier + ++ # The width of parts of the servo memory address depends on the number ++ # of channels. ++ self.num_channels = 4 * len(dds_devices) ++ channel_width = ceil(log2(self.num_channels)) ++ coeff_depth = PROFILE_WIDTH + channel_width + 3 ++ self.state_sel = 2 << (coeff_depth - 2) ++ self.config_addr = 3 << (coeff_depth - 2) ++ self.coeff_sel = 1 << coeff_depth ++ self.write_enable = 1 << (coeff_depth + 1) ++ + @kernel + def init(self): +- """Initialize the servo, Sampler and both Urukuls. ++ """Initialize the servo, Sampler and all Urukuls. + + Leaves the servo disabled (see :meth:`set_config`), resets and + configures all DDS. +@@ -122,7 +130,7 @@ def write(self, addr, value): + :param addr: Memory location address. + :param value: Data to be written. + """ +- addr |= WE ++ addr |= self.write_enable + value &= (1 << COEFF_WIDTH) - 1 + value |= (addr >> 8) << COEFF_WIDTH + addr = addr & 0xff +@@ -158,7 +166,7 @@ def set_config(self, enable): + Disabling takes up to two servo cycles (~2.3 µs) to clear the + processing pipeline. + """ +- self.write(CONFIG_ADDR, enable) ++ self.write(self.config_addr, enable) + + @kernel + def get_status(self): +@@ -179,7 +187,7 @@ def get_status(self): + :return: Status. Bit 0: enabled, bit 1: done, + bits 8-15: channel clip indicators. + """ +- return self.read(CONFIG_ADDR) ++ return self.read(self.config_addr) + + @kernel + def get_adc_mu(self, adc): +@@ -197,7 +205,8 @@ def get_adc_mu(self, adc): + # State memory entries are 25 bits. Due to the pre-adder dynamic + # range, X0/X1/OFFSET are only 24 bits. Finally, the RTIO interface + # only returns the 18 MSBs (the width of the coefficient memory). +- return self.read(STATE_SEL | (adc << 1) | (1 << 8)) ++ return self.read(self.state_sel | ++ (2 * adc + (1 << PROFILE_WIDTH) * self.num_channels)) + + @kernel + def set_pgia_mu(self, channel, gain): +@@ -285,10 +294,11 @@ def set_dds_mu(self, profile, ftw, offs, pow_=0): + :param offs: IIR offset (17 bit signed) + :param pow_: Phase offset word (16 bit) + """ +- base = (self.servo_channel << 8) | (profile << 3) ++ base = self.servo.coeff_sel | (self.servo_channel << ++ (3 + PROFILE_WIDTH)) | (profile << 3) + self.servo.write(base + 0, ftw >> 16) + self.servo.write(base + 6, (ftw & 0xffff)) +- self.set_dds_offset_mu(profile, offs) ++ self.servo.write(base + 4, offs) + self.servo.write(base + 2, pow_) + + @kernel +@@ -319,7 +329,8 @@ def set_dds_offset_mu(self, profile, offs): + :param profile: Profile number (0-31) + :param offs: IIR offset (17 bit signed) + """ +- base = (self.servo_channel << 8) | (profile << 3) ++ base = self.servo.coeff_sel | (self.servo_channel << ++ (3 + PROFILE_WIDTH)) | (profile << 3) + self.servo.write(base + 4, offs) + + @kernel +@@ -344,6 +355,30 @@ def dds_offset_to_mu(self, offset): + """ + return int(round(offset * (1 << COEFF_WIDTH - 1))) + ++ @kernel ++ def set_dds_phase_mu(self, profile, pow_): ++ """Set only POW in profile DDS coefficients. ++ ++ See :meth:`set_dds_mu` for setting the complete DDS profile. ++ ++ :param profile: Profile number (0-31) ++ :param pow_: Phase offset word (16 bit) ++ """ ++ base = self.servo.coeff_sel | (self.servo_channel << ++ (3 + PROFILE_WIDTH)) | (profile << 3) ++ self.servo.write(base + 2, pow_) ++ ++ @kernel ++ def set_dds_phase(self, profile, phase): ++ """Set only phase in profile DDS coefficients. ++ ++ See :meth:`set_dds` for setting the complete DDS profile. ++ ++ :param profile: Profile number (0-31) ++ :param phase: DDS phase in turns ++ """ ++ self.set_dds_phase_mu(profile, self.dds.turns_to_pow(phase)) ++ + @kernel + def set_iir_mu(self, profile, adc, a1, b0, b1, dly=0): + """Set profile IIR coefficients in machine units. +@@ -378,7 +413,8 @@ def set_iir_mu(self, profile, adc, a1, b0, b1, dly=0): + :param dly: IIR update suppression time. In units of IIR cycles + (~1.2 µs, 0-255). + """ +- base = (self.servo_channel << 8) | (profile << 3) ++ base = self.servo.coeff_sel | (self.servo_channel << ++ (3 + PROFILE_WIDTH)) | (profile << 3) + self.servo.write(base + 3, adc | (dly << 8)) + self.servo.write(base + 1, b1) + self.servo.write(base + 5, a1) +@@ -470,7 +506,9 @@ def get_profile_mu(self, profile, data): + :param profile: Profile number (0-31) + :param data: List of 8 integers to write the profile data into + """ +- base = (self.servo_channel << 8) | (profile << 3) ++ assert len(data) == 8 ++ base = self.servo.coeff_sel | (self.servo_channel << ++ (3 + PROFILE_WIDTH)) | (profile << 3) + for i in range(len(data)): + data[i] = self.servo.read(base + i) + delay(4*us) +@@ -491,7 +529,8 @@ def get_y_mu(self, profile): + :param profile: Profile number (0-31) + :return: 17 bit unsigned Y0 + """ +- return self.servo.read(STATE_SEL | (self.servo_channel << 5) | profile) ++ return self.servo.read(self.servo.state_sel | ( ++ self.servo_channel << PROFILE_WIDTH) | profile) + + @kernel + def get_y(self, profile): +@@ -529,7 +568,8 @@ def set_y_mu(self, profile, y): + """ + # State memory is 25 bits wide and signed. + # Reads interact with the 18 MSBs (coefficient memory width) +- self.servo.write(STATE_SEL | (self.servo_channel << 5) | profile, y) ++ self.servo.write(self.servo.state_sel | ( ++ self.servo_channel << PROFILE_WIDTH) | profile, y) + + @kernel + def set_y(self, profile, y): +diff --git a/artiq/gateware/eem.py b/artiq/gateware/eem.py +index 7f5fe3fdf..fbfdafe7d 100644 +--- a/artiq/gateware/eem.py ++++ b/artiq/gateware/eem.py +@@ -473,11 +473,10 @@ def add_std(cls, target, eem, eem_aux=None, eem_aux2=None, ttl_out_cls=None, + class SUServo(_EEM): + @staticmethod + def io(*eems, iostandard): +- assert len(eems) in (4, 6) +- io = (Sampler.io(*eems[0:2], iostandard=iostandard) +- + Urukul.io_qspi(*eems[2:4], iostandard=iostandard)) +- if len(eems) == 6: # two Urukuls +- io += Urukul.io_qspi(*eems[4:6], iostandard=iostandard) ++ assert len(eems) >= 4 and len(eems) % 2 == 0 ++ io = Sampler.io(*eems[0:2], iostandard=iostandard) ++ for i in range(len(eems) // 2 - 1): ++ io += Urukul.io_qspi(*eems[(2 * i + 2):(2 * i + 4)], iostandard=iostandard) + return io + + @classmethod +@@ -516,10 +515,9 @@ def add_std(cls, target, eems_sampler, eems_urukul, + # difference (4 cycles measured) + t_conv=57 - 4, t_rtt=t_rtt + 4) + iir_p = servo.IIRWidths(state=25, coeff=18, adc=16, asf=14, word=16, +- accu=48, shift=shift, channel=3, +- profile=profile, dly=8) ++ accu=48, shift=shift, profile=profile, dly=8) + dds_p = servo.DDSParams(width=8 + 32 + 16 + 16, +- channels=adc_p.channels, clk=clk) ++ channels=4 * len(eem_urukul), clk=clk) + su = servo.Servo(sampler_pads, urukul_pads, adc_p, iir_p, dds_p) + su = ClockDomainsRenamer("rio_phy")(su) + # explicitly name the servo submodule to enable the migen namer to derive +@@ -540,27 +538,23 @@ def add_std(cls, target, eems_sampler, eems_urukul, + target.submodules += phy + target.rtio_channels.append(rtio.Channel.from_phy(phy, ififo_depth=4)) + +- for i in range(2): +- if len(eem_urukul) > i: +- spi_p, spi_n = ( +- target.platform.request("{}_spi_p".format(eem_urukul[i])), +- target.platform.request("{}_spi_n".format(eem_urukul[i]))) +- else: # create a dummy bus +- spi_p = Record([("clk", 1), ("cs_n", 1)]) # mosi, cs_n +- spi_n = None +- ++ dds_sync = Signal(reset=0) ++ for j, eem_urukuli in enumerate(eem_urukul): ++ # connect quad-SPI ++ spi_p, spi_n = ( ++ target.platform.request("{}_spi_p".format(eem_urukuli)), ++ target.platform.request("{}_spi_n".format(eem_urukuli))) + phy = spi2.SPIMaster(spi_p, spi_n) + target.submodules += phy + target.rtio_channels.append(rtio.Channel.from_phy(phy, ififo_depth=4)) +- +- for j, eem_urukuli in enumerate(eem_urukul): ++ # connect `reset_sync_in` + pads = target.platform.request("{}_dds_reset_sync_in".format(eem_urukuli)) +- target.specials += DifferentialOutput(0, pads.p, pads.n) +- ++ target.specials += DifferentialOutput(dds_sync, pads.p, pads.n) ++ # connect RF switches + for i, signal in enumerate("sw0 sw1 sw2 sw3".split()): + pads = target.platform.request("{}_{}".format(eem_urukuli, signal)) + target.specials += DifferentialOutput( +- su.iir.ctrl[j*4 + i].en_out, pads.p, pads.n) ++ su.iir.ctrl[j * 4 + i].en_out, pads.p, pads.n) + + + class Mirny(_EEM): +diff --git a/artiq/gateware/rtio/phy/servo.py b/artiq/gateware/rtio/phy/servo.py +index 9fa634521..379e7ba32 100644 +--- a/artiq/gateware/rtio/phy/servo.py ++++ b/artiq/gateware/rtio/phy/servo.py +@@ -34,28 +34,38 @@ class RTServoMem(Module): + """All-channel all-profile coefficient and state RTIO control + interface. + ++ The real-time interface exposes the following functions: ++ 1. enable/disable servo iterations ++ 2. read the servo status (including state of clip register) ++ 3. access the IIR coefficient memory (set PI loop gains etc.) ++ 4. access the IIR state memory (set offset and read ADC data) ++ ++ The bit assignments for the servo address space are (from MSB): ++ * write-enable (1 bit) ++ * sel_coeff (1 bit) ++ If selected, the coefficient memory location is ++ addressed by all the lower bits excluding the LSB (high_coeff). ++ - high_coeff (1 bit) selects between the upper and lower halves of that ++ memory location. ++ Else (if ~sel_coeff), the following bits are: ++ - sel (2 bits) selects between the following memory locations: ++ ++ destination | sel | sel_coeff | ++ ----------------|-------|--------------| ++ IIR coeff mem | - | 1 | ++ Reserved | 1 | 0 | ++ IIR state mem | 2 | 0 | ++ config (write) | 3 | 0 | ++ status (read) | 3 | 0 | ++ ++ - IIR state memory address ++ + Servo internal addresses are internal_address_width wide, which is + typically longer than the 8-bit RIO address space. We pack the overflow + onto the RTIO data word after the data. + +- Servo address space (from LSB): +- - IIR coefficient/state memory address, (w.profile + w.channel + 2) bits. +- If the state memory is selected, the lower bits are used directly as +- the memory address. If the coefficient memory is selected, the LSB +- (high_coeff) selects between the upper and lower halves of the memory +- location, which is two coefficients wide, with the remaining bits used +- as the memory address. +- - config_sel (1 bit) +- - state_sel (1 bit) +- - we (1 bit) +- +- destination | config_sel | state_sel +- ----------------|------------|---------- +- IIR coeff mem | 0 | 0 +- IIR coeff mem | 1 | 0 +- IIR state mem | 0 | 1 +- config (write) | 1 | 1 +- status (read) | 1 | 1 ++ The address layout reflects the fact that typically, the coefficient memory ++ address is 2 bits wider than the state memory address. + + Values returned to the user on the Python side of the RTIO interface are + 32 bit, so we sign-extend all values from w.coeff to that width. This works +@@ -71,6 +81,7 @@ def __init__(self, w, servo): + # mode=READ_FIRST, + clock_domain="rio") + self.specials += m_state, m_coeff ++ w_channel = bits_for(len(servo.iir.dds) - 1) + + # just expose the w.coeff (18) MSBs of state + assert w.state >= w.coeff +@@ -83,7 +94,7 @@ def __init__(self, w, servo): + assert 8 + w.dly < w.coeff + + # coeff, profile, channel, 2 mems, rw +- internal_address_width = 3 + w.profile + w.channel + 1 + 1 ++ internal_address_width = 3 + w.profile + w_channel + 1 + 1 + rtlink_address_width = min(8, internal_address_width) + overflow_address_width = internal_address_width - rtlink_address_width + self.rtlink = rtlink.Interface( +@@ -99,8 +110,9 @@ def __init__(self, w, servo): + # # # + + config = Signal(w.coeff, reset=0) +- status = Signal(w.coeff) ++ status = Signal(8 + len(servo.iir.ctrl)) + pad = Signal(6) ++ assert len(status) <= len(self.rtlink.i.data) + self.comb += [ + Cat(servo.start).eq(config), + status.eq(Cat(servo.start, servo.done, pad, +@@ -109,15 +121,19 @@ def __init__(self, w, servo): + + assert len(self.rtlink.o.address) + len(self.rtlink.o.data) - w.coeff == ( + 1 + # we +- 1 + # state_sel ++ 1 + # sel_coeff + 1 + # high_coeff + len(m_coeff.adr)) + # ensure that we can fit config/status into the state address space + assert len(self.rtlink.o.address) + len(self.rtlink.o.data) - w.coeff >= ( + 1 + # we +- 1 + # state_sel +- 1 + # config_sel ++ 1 + # sel_coeff ++ 2 + # sel + len(m_state.adr)) ++ # ensure that IIR state mem addresses are at least 2 bits less wide than ++ # IIR coeff mem addresses to ensure we can fit SEL after the state mem ++ # address and before the SEL_COEFF bit. ++ assert w.profile + w_channel >= 4 + + internal_address = Signal(internal_address_width) + self.comb += internal_address.eq(Cat(self.rtlink.o.address, +@@ -127,52 +143,51 @@ def __init__(self, w, servo): + self.comb += coeff_data.eq(self.rtlink.o.data[:w.coeff]) + + we = internal_address[-1] +- state_sel = internal_address[-2] +- config_sel = internal_address[-3] ++ sel_coeff = internal_address[-2] ++ sel1 = internal_address[-3] ++ sel0 = internal_address[-4] + high_coeff = internal_address[0] ++ sel = Signal(2) + self.comb += [ + self.rtlink.o.busy.eq(0), ++ sel.eq(Mux(sel_coeff, 0, Cat(sel0, sel1))), + m_coeff.adr.eq(internal_address[1:]), + m_coeff.dat_w.eq(Cat(coeff_data, coeff_data)), +- m_coeff.we[0].eq(self.rtlink.o.stb & ~high_coeff & +- we & ~state_sel), +- m_coeff.we[1].eq(self.rtlink.o.stb & high_coeff & +- we & ~state_sel), ++ m_coeff.we[0].eq(self.rtlink.o.stb & ~high_coeff & we & sel_coeff), ++ m_coeff.we[1].eq(self.rtlink.o.stb & high_coeff & we & sel_coeff), + m_state.adr.eq(internal_address), + m_state.dat_w[w.state - w.coeff:].eq(self.rtlink.o.data), +- m_state.we.eq(self.rtlink.o.stb & we & state_sel & ~config_sel), ++ m_state.we.eq(self.rtlink.o.stb & we & (sel == 2)), + ] + read = Signal() +- read_state = Signal() + read_high = Signal() +- read_config = Signal() ++ read_sel = Signal(2) + self.sync.rio += [ + If(read, + read.eq(0) + ), + If(self.rtlink.o.stb, + read.eq(~we), +- read_state.eq(state_sel), ++ read_sel.eq(sel), + read_high.eq(high_coeff), +- read_config.eq(config_sel), + ) + ] + self.sync.rio_phy += [ +- If(self.rtlink.o.stb & we & state_sel & config_sel, ++ If(self.rtlink.o.stb & we & (sel == 3), + config.eq(self.rtlink.o.data) + ), +- If(read & read_config & read_state, ++ If(read & (read_sel == 3), + [_.clip.eq(0) for _ in servo.iir.ctrl] +- ) ++ ), + ] ++ # read return value by destination ++ read_acts = Array([ ++ Mux(read_high, m_coeff.dat_r[w.coeff:], m_coeff.dat_r[:w.coeff]), ++ 0, ++ m_state.dat_r[w.state - w.coeff:], ++ status ++ ]) + self.comb += [ + self.rtlink.i.stb.eq(read), +- _eq_sign_extend(self.rtlink.i.data, +- Mux(read_state, +- Mux(read_config, +- status, +- m_state.dat_r[w.state - w.coeff:]), +- Mux(read_high, +- m_coeff.dat_r[w.coeff:], +- m_coeff.dat_r[:w.coeff]))) ++ _eq_sign_extend(self.rtlink.i.data, read_acts[read_sel]), + ] +diff --git a/artiq/gateware/suservo/iir.py b/artiq/gateware/suservo/iir.py +index 0ec9bfa09..6b975b753 100644 +--- a/artiq/gateware/suservo/iir.py ++++ b/artiq/gateware/suservo/iir.py +@@ -16,7 +16,6 @@ + "word", # "word" size to break up DDS profile data (16) + "asf", # unsigned amplitude scale factor for DDS (14) + "shift", # fixed point scaling coefficient for a1, b0, b1 (log2!) (11) +- "channel", # channels (log2!) (3) + "profile", # profiles per channel (log2!) (5) + "dly", # the activation delay + ]) +@@ -213,10 +212,10 @@ class IIR(Module): + --/--: signal with a given bit width always includes a sign bit + -->--: flow is to the right and down unless otherwise indicated + """ +- def __init__(self, w): +- self.widths = w +- for i, j in enumerate(w): +- assert j > 0, (i, j, w) ++ def __init__(self, w, w_i, w_o): ++ for v in (w, w_i, w_o): ++ for i, j in enumerate(v): ++ assert j > 0, (i, j, v) + assert w.word <= w.coeff # same memory + assert w.state + w.coeff + 3 <= w.accu + +@@ -224,13 +223,13 @@ def __init__(self, w): + # ~processing + self.specials.m_coeff = Memory( + width=2*w.coeff, # Cat(pow/ftw/offset, cfg/a/b) +- depth=4 << w.profile + w.channel) ++ depth=(4 << w.profile) * w_o.channels) + # m_state[x] should only be read externally during ~(shifting | loading) + # m_state[y] of active profiles should only be read externally during + # ~processing + self.specials.m_state = Memory( + width=w.state, # y1,x0,x1 +- depth=(1 << w.profile + w.channel) + (2 << w.channel)) ++ depth=(1 << w.profile) * w_o.channels + 2 * w_i.channels) + # ctrl should only be updated synchronously + self.ctrl = [Record([ + ("profile", w.profile), +@@ -238,14 +237,14 @@ def __init__(self, w): + ("en_iir", 1), + ("clip", 1), + ("stb", 1)]) +- for i in range(1 << w.channel)] ++ for i in range(w_o.channels)] + # only update during ~loading + self.adc = [Signal((w.adc, True), reset_less=True) +- for i in range(1 << w.channel)] ++ for i in range(w_i.channels)] + # Cat(ftw0, ftw1, pow, asf) + # only read externally during ~processing +- self.dds = [Signal(4*w.word, reset_less=True) +- for i in range(1 << w.channel)] ++ self.dds = [Signal(4 * w.word, reset_less=True) ++ for i in range(w_o.channels)] + # perform one IIR iteration, start with loading, + # then processing, then shifting, end with done + self.start = Signal() +@@ -281,7 +280,7 @@ def __init__(self, w): + # using the (MSBs of) t_current_step, and, after all channels have been + # covered, proceed once the pipeline has completely drained. + self.submodules.fsm = fsm = FSM("IDLE") +- t_current_step = Signal(w.channel + 2) ++ t_current_step = Signal(max=max(4 * (w_o.channels + 2), 2 * w_i.channels)) + t_current_step_clr = Signal() + + # pipeline group activity flags (SR) +@@ -298,7 +297,7 @@ def __init__(self, w): + ) + fsm.act("LOAD", + self.loading.eq(1), +- If(t_current_step == (1 << w.channel) - 1, ++ If(t_current_step == w_i.channels - 1, + t_current_step_clr.eq(1), + NextValue(stages_active[0], 1), + NextState("PROCESS") +@@ -315,7 +314,7 @@ def __init__(self, w): + ) + fsm.act("SHIFT", + self.shifting.eq(1), +- If(t_current_step == (2 << w.channel) - 1, ++ If(t_current_step == 2 * w_i.channels - 1, + NextState("IDLE") + ) + ) +@@ -333,13 +332,13 @@ def __init__(self, w): + # pipeline group channel pointer (SR) + # for each pipeline stage, this is the channel currently being + # processed +- channel = [Signal(w.channel, reset_less=True) for i in range(3)] ++ channel = [Signal(max=w_o.channels, reset_less=True) for i in range(3)] + self.comb += Cat(pipeline_phase, channel[0]).eq(t_current_step) + self.sync += [ + If(pipeline_phase == 3, + Cat(channel[1:]).eq(Cat(channel[:-1])), + stages_active[1:].eq(stages_active[:-1]), +- If(channel[0] == (1 << w.channel) - 1, ++ If(channel[0] == w_o.channels - 1, + stages_active[0].eq(0) + ) + ) +@@ -393,13 +392,13 @@ def __init__(self, w): + + # selected adc and profile delay (combinatorial from dat_r) + # both share the same coeff word (sel in the lower 8 bits) +- sel_profile = Signal(w.channel) ++ sel_profile = Signal(max=w_i.channels) + dly_profile = Signal(w.dly) +- assert w.channel <= 8 ++ assert w_o.channels < (1 << 8) + assert 8 + w.dly <= w.coeff + + # latched adc selection +- sel = Signal(w.channel, reset_less=True) ++ sel = Signal(max=w_i.channels, reset_less=True) + # iir enable SR + en = Signal(2, reset_less=True) + +@@ -407,12 +406,12 @@ def __init__(self, w): + sel_profile.eq(m_coeff.dat_r[w.coeff:]), + dly_profile.eq(m_coeff.dat_r[w.coeff + 8:]), + If(self.shifting, +- m_state.adr.eq(t_current_step | (1 << w.profile + w.channel)), ++ m_state.adr.eq(t_current_step + (1 << w.profile) * w_o.channels), + m_state.dat_w.eq(m_state.dat_r), + m_state.we.eq(t_current_step[0]) + ), + If(self.loading, +- m_state.adr.eq((t_current_step << 1) | (1 << w.profile + w.channel)), ++ m_state.adr.eq((t_current_step << 1) + (1 << w.profile) * w_o.channels), + m_state.dat_w[-w.adc - 1:-1].eq(Array(self.adc)[t_current_step]), + m_state.dat_w[-1].eq(m_state.dat_w[-2]), + m_state.we.eq(1) +@@ -424,9 +423,9 @@ def __init__(self, w): + # read old y + Cat(profile[0], channel[0]), + # read x0 (recent) +- 0 | (sel_profile << 1) | (1 << w.profile + w.channel), ++ 0 | (sel_profile << 1) + (1 << w.profile) * w_o.channels, + # read x1 (old) +- 1 | (sel << 1) | (1 << w.profile + w.channel), ++ 1 | (sel << 1) + (1 << w.profile) * w_o.channels, + ])[pipeline_phase]), + m_state.dat_w.eq(dsp.output), + m_state.we.eq((pipeline_phase == 0) & stages_active[2] & en[1]), +@@ -438,11 +437,9 @@ def __init__(self, w): + # + + # internal channel delay counters +- dlys = Array([Signal(w.dly) +- for i in range(1 << w.channel)]) +- self._dlys = dlys # expose for debugging only ++ dlys = Array([Signal(w.dly) for i in range(w_o.channels)]) + +- for i in range(1 << w.channel): ++ for i in range(w_o.channels): + self.sync += [ + # (profile != profile_old) | ~en_out + If(self.ctrl[i].stb, +@@ -517,6 +514,12 @@ def __init__(self, w): + }), + ] + ++ # expose for simulation and debugging only ++ self.widths = w ++ self.widths_adc = w_i ++ self.widths_dds = w_o ++ self._dlys = dlys ++ + def _coeff(self, channel, profile, coeff): + """Return ``high_word``, ``address`` and bit ``mask`` for the + storage of coefficient name ``coeff`` in profile ``profile`` +@@ -564,31 +567,33 @@ def get_coeff(self, channel, profile, coeff): + def set_state(self, channel, val, profile=None, coeff="y1"): + """Set a state value.""" + w = self.widths ++ w_o = self.widths_dds + if coeff == "y1": + assert profile is not None + yield self.m_state[profile | (channel << w.profile)].eq(val) + elif coeff == "x0": + assert profile is None +- yield self.m_state[(channel << 1) | +- (1 << w.profile + w.channel)].eq(val) ++ yield self.m_state[(channel << 1) + ++ (1 << w.profile) * w_o.channels].eq(val) + elif coeff == "x1": + assert profile is None +- yield self.m_state[1 | (channel << 1) | +- (1 << w.profile + w.channel)].eq(val) ++ yield self.m_state[1 | (channel << 1) + ++ (1 << w.profile) * w_o.channels].eq(val) + else: + raise ValueError("no such state", coeff) + + def get_state(self, channel, profile=None, coeff="y1"): + """Get a state value.""" + w = self.widths ++ w_o = self.widths_dds + if coeff == "y1": + val = yield self.m_state[profile | (channel << w.profile)] + elif coeff == "x0": +- val = yield self.m_state[(channel << 1) | +- (1 << w.profile + w.channel)] ++ val = yield self.m_state[(channel << 1) + ++ (1 << w.profile) * w_o.channels] + elif coeff == "x1": +- val = yield self.m_state[1 | (channel << 1) | +- (1 << w.profile + w.channel)] ++ val = yield self.m_state[1 | (channel << 1) + ++ (1 << w.profile) * w_o.channels] + else: + raise ValueError("no such state", coeff) + return signed(val, w.state) +@@ -607,6 +612,8 @@ def check_iter(self): + """Perform a single processing iteration while verifying + the behavior.""" + w = self.widths ++ w_i = self.widths_adc ++ w_o = self.widths_dds + + while not (yield self.done): + yield +@@ -622,7 +629,7 @@ def check_iter(self): + + x0s = [] + # check adc loading +- for i in range(1 << w.channel): ++ for i in range(w_i.channels): + v_adc = signed((yield self.adc[i]), w.adc) + x0 = yield from self.get_state(i, coeff="x0") + x0s.append(x0) +@@ -631,7 +638,7 @@ def check_iter(self): + + data = [] + # predict output +- for i in range(1 << w.channel): ++ for i in range(w_o.channels): + j = yield self.ctrl[i].profile + en_iir = yield self.ctrl[i].en_iir + en_out = yield self.ctrl[i].en_out +@@ -640,7 +647,7 @@ def check_iter(self): + i, j, en_iir, en_out, dly_i) + + cfg = yield from self.get_coeff(i, j, "cfg") +- k_j = cfg & ((1 << w.channel) - 1) ++ k_j = cfg & ((1 << bits_for(w_i.channels - 1)) - 1) + dly_j = (cfg >> 8) & 0xff + logger.debug("cfg[%d,%d] sel=%d dly=%d", i, j, k_j, dly_j) + +@@ -694,7 +701,7 @@ def check_iter(self): + logger.debug("adc[%d] x0=%x x1=%x", i, x0, x1) + + # check new state +- for i in range(1 << w.channel): ++ for i in range(w_o.channels): + j = yield self.ctrl[i].profile + logger.debug("ch[%d] profile=%d", i, j) + y1 = yield from self.get_state(i, j, "y1") +@@ -702,7 +709,7 @@ def check_iter(self): + assert y1 == y0, (hex(y1), hex(y0)) + + # check dds output +- for i in range(1 << w.channel): ++ for i in range(w_o.channels): + ftw0, ftw1, pow, y0, x1, x0 = data[i] + asf = y0 >> (w.state - w.asf - 1) + dds = (ftw0 | (ftw1 << w.word) | +diff --git a/artiq/gateware/suservo/pads.py b/artiq/gateware/suservo/pads.py +index 0ab7d352f..778f05d01 100644 +--- a/artiq/gateware/suservo/pads.py ++++ b/artiq/gateware/suservo/pads.py +@@ -72,12 +72,11 @@ def __init__(self, platform, *eems): + DifferentialOutput(self.clk, spip[i].clk, spin[i].clk), + DifferentialOutput(self.io_update, ioup[i].p, ioup[i].n)) + for i in range(len(eems))] +- for i in range(8): ++ for i in range(4 * len(eems)): + mosi = Signal() + setattr(self, "mosi{}".format(i), mosi) +- for i in range(4*len(eems)): + self.specials += [ +- DifferentialOutput(getattr(self, "mosi{}".format(i)), ++ DifferentialOutput(mosi, + getattr(spip[i // 4], "mosi{}".format(i % 4)), + getattr(spin[i // 4], "mosi{}".format(i % 4))) + ] +diff --git a/artiq/gateware/suservo/servo.py b/artiq/gateware/suservo/servo.py +index 1aec95f02..59529320c 100644 +--- a/artiq/gateware/suservo/servo.py ++++ b/artiq/gateware/suservo/servo.py +@@ -42,7 +42,7 @@ def __init__(self, adc_pads, dds_pads, adc_p, iir_p, dds_p): + assert t_iir + 2*adc_p.channels < t_cycle, "need shifting time" + + self.submodules.adc = ADC(adc_pads, adc_p) +- self.submodules.iir = IIR(iir_p) ++ self.submodules.iir = IIR(iir_p, adc_p, dds_p) + self.submodules.dds = DDS(dds_pads, dds_p) + + # adc channels are reversed on Sampler From c26fa5eb90a3cafc9829e4eeea2ba76a9857cf26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Mon, 5 Sep 2022 20:48:15 +0000 Subject: [PATCH 23/72] err out on tune_fifo_offset --- artiq/coredevice/phaser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index caadc8fdc..472d8146f 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -791,6 +791,8 @@ class Phaser: if good & (1 << o): sum += o count += 1 + if count == 0: + raise ValueError("no good fifo offset") best = ((sum // count) + offset) % 8 self.dac_write(0x09, (config9 & 0x1fff) | (best << 13)) return best From 27e3c044ed28293b8f3df28cac4de7785e128c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 6 Sep 2022 14:32:57 +0000 Subject: [PATCH 24/72] fix dt computation --- artiq/coredevice/phaser.py | 6 +++--- artiq/gateware/rtio/phy/phaser.py | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 472d8146f..18b1182d4 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -1469,13 +1469,13 @@ class Miqro: data[0] = window word = 0 idx = 10 - for i in range(len(profiles)): - if profiles[i] > 0x1f: + for profile in profiles: + if profile > 0x1f: raise ValueError("profile out of bounds") if idx >= 30: word += 1 idx = 0 - data[word] |= (profiles[i] & 0x1f) << idx + data[word] |= profile << idx idx += 5 return word diff --git a/artiq/gateware/rtio/phy/phaser.py b/artiq/gateware/rtio/phy/phaser.py index 1a1ced03d..4ff1e7d42 100644 --- a/artiq/gateware/rtio/phy/phaser.py +++ b/artiq/gateware/rtio/phy/phaser.py @@ -99,6 +99,7 @@ class MiqroChannel(Module): self.ack = Signal() regs = [Signal(30, reset_less=True) for _ in range(3)] dt = Signal(7, reset_less=True) + dt_frame = Signal(6, reset_less=True) stb = Signal() pulse = Cat(stb, dt, regs) assert len(self.pulse) >= len(pulse) @@ -107,18 +108,18 @@ class MiqroChannel(Module): self.rtlink.o.busy.eq(stb & ~self.ack), ] self.sync.rtio += [ - dt.eq(dt + 2), + dt_frame.eq(dt_frame + 1), If(self.ack, - dt[1:].eq(0), - stb.eq(0), + dt_frame.eq(0), If(stb, [r.eq(0) for r in regs], ), + stb.eq(0), ), If(self.rtlink.o.stb, Array(regs)[self.rtlink.o.address].eq(self.rtlink.o.data), If(self.rtlink.o.address == 0, - dt[0].eq(self.rtlink.o.fine_ts), + dt.eq(Cat(self.rtlink.o.fine_ts, dt_frame)), stb.eq(1), ), ), From c5c5c3061703bb2dd102d9962d26df50d9d95eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 6 Sep 2022 15:42:05 +0000 Subject: [PATCH 25/72] add set_window(), clean up api --- artiq/coredevice/phaser.py | 66 +++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 18b1182d4..9db6e84b5 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -51,6 +51,9 @@ PHASER_ADDR_SERVO_DATA_BASE = 0x32 PHASER_ADDR_MIQRO_MEM_ADDR = 0x72 PHASER_ADDR_MIQRO_MEM_DATA = 0x74 +# Miqro profile memory select +PHASER_MIQRO_SEL_PROFILE = 1 << 14 + PHASER_SEL_DAC = 1 << 0 PHASER_SEL_TRF0 = 1 << 1 PHASER_SEL_TRF1 = 1 << 2 @@ -1405,8 +1408,7 @@ class Miqro: for pro in range(32): self.set_profile_mu(osc, pro, 0, 0, 0) delay(20*us) - self.set_window_mu( - start=0, data=[[0, 0]], rate=1, shift=0, order=0, head=0, tail=1) + self.set_window_mu(start=0, iq=[0], rate=1, shift=0, order=0, head=0, tail=1) self.pulse(window=0, profiles=[0]) @kernel @@ -1416,8 +1418,8 @@ class Miqro: if profile >= 32: raise ValueError("invalid profile index") self.channel.phaser.write16(PHASER_ADDR_MIQRO_MEM_ADDR, - (self.channel.index << 15) | (1 << 14) | (oscillator << 6) | - (profile << 1)) + (self.channel.index << 15) | PHASER_MIQRO_SEL_PROFILE | + (oscillator << 6) | (profile << 1)) self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, ftw) self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, (asf & 0xffff) | (pow << 16)) @@ -1433,10 +1435,10 @@ class Miqro: self.set_profile_mu(oscillator, profile, ftw, asf, pow) @kernel - def set_window_mu(self, start, data, rate=1, shift=0, order=0, head=0, tail=0): + def set_window_mu(self, start, iq, rate=1, shift=0, order=3, head=1, tail=1): if start >= 1 << 10: raise ValueError("start out of bouncs") - if len(data) >= 1 << 10: + if len(iq) >= 1 << 10: raise ValueError("window length out of bounds") if rate < 1 or rate > 1 << 12: raise ValueError("rate out of bounds") @@ -1447,18 +1449,35 @@ class Miqro: self.channel.phaser.write16(PHASER_ADDR_MIQRO_MEM_ADDR, (self.channel.index << 15) | start) self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, - ((start + 1 + len(data)) & 0x3ff) - | ((rate - 1) << 10) - | (shift << 22) - | (order << 28) - | ((head & 1) << 30) - | ((tail & 1) << 31) + ((start + 1 + len(iq)) & 0x3ff) | + ((rate - 1) << 10) | + (shift << 22) | + (order << 28) | + ((head & 1) << 30) | + ((tail & 1) << 31) ) - for d in data: - self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, - (d[0] & 0xffff) | (d[1] << 16)) - delay(10*us) - return (start + 1 + len(data)) & 0x3ff + for iqi in iq: + self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, iqi) + delay(10*us) # slack for long windows + return (start + 1 + len(iq)) & 0x3ff + + @kernel + def set_window(self, start, iq, period=4*ns, order=3, head=1, tail=1): + rate = int32(round(period/(4*ns))) + gain = 1. + for _ in range(order): + gain *= rate + shift = 0 + while gain >= 2.: + shift += 1 + gain *= .5 + scale = ((1 << 15) - 1)/gain + iq_mu = [ + (int32(round(iqi[0]*scale)) & 0xffff) | + (int32(round(iqi[1]*scale)) << 16) + for iqi in iq + ] + self.set_window_mu(start, iq_mu, rate, shift, order, head, tail) @kernel def encode(self, window, profiles, data): @@ -1477,17 +1496,20 @@ class Miqro: idx = 0 data[word] |= profile << idx idx += 5 - return word + return word + 1 @kernel def pulse_mu(self, data): - for word in range(len(data) - 1, -1, -1): - rtio_output(self.base_addr + word, data[word]) + word = len(data) + delay_mu(-8*word) # back shift to align + while word > 0: delay_mu(8) + word -= 1 + # final write sets pulse stb + rtio_output(self.base_addr + word, data[word]) @kernel def pulse(self, window, profiles): data = [0, 0, 0] words = self.encode(window, profiles, data) - delay_mu(-8*words) - self.pulse_mu(data[:words + 1]) + self.pulse_mu(data[:words]) From a91836e5fe75476b6fb7041ba93cc2013eb3eb1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 6 Sep 2022 20:26:50 +0000 Subject: [PATCH 26/72] easier fix for dt --- artiq/gateware/rtio/phy/phaser.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/artiq/gateware/rtio/phy/phaser.py b/artiq/gateware/rtio/phy/phaser.py index 4ff1e7d42..d4658e828 100644 --- a/artiq/gateware/rtio/phy/phaser.py +++ b/artiq/gateware/rtio/phy/phaser.py @@ -99,7 +99,6 @@ class MiqroChannel(Module): self.ack = Signal() regs = [Signal(30, reset_less=True) for _ in range(3)] dt = Signal(7, reset_less=True) - dt_frame = Signal(6, reset_less=True) stb = Signal() pulse = Cat(stb, dt, regs) assert len(self.pulse) >= len(pulse) @@ -108,9 +107,11 @@ class MiqroChannel(Module): self.rtlink.o.busy.eq(stb & ~self.ack), ] self.sync.rtio += [ - dt_frame.eq(dt_frame + 1), + If(~stb, + dt.eq(dt + 2), + ), If(self.ack, - dt_frame.eq(0), + dt.eq(0), If(stb, [r.eq(0) for r in regs], ), @@ -119,7 +120,7 @@ class MiqroChannel(Module): If(self.rtlink.o.stb, Array(regs)[self.rtlink.o.address].eq(self.rtlink.o.data), If(self.rtlink.o.address == 0, - dt.eq(Cat(self.rtlink.o.fine_ts, dt_frame)), + dt[0].eq(self.rtlink.o.fine_ts), stb.eq(1), ), ), From 857fb4ececf9f93981b1c8f1faef6cc5096a700a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 6 Sep 2022 20:44:47 +0000 Subject: [PATCH 27/72] spelling --- artiq/coredevice/phaser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 9db6e84b5..4b3741344 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -1437,7 +1437,7 @@ class Miqro: @kernel def set_window_mu(self, start, iq, rate=1, shift=0, order=3, head=1, tail=1): if start >= 1 << 10: - raise ValueError("start out of bouncs") + raise ValueError("start out of bounds") if len(iq) >= 1 << 10: raise ValueError("window length out of bounds") if rate < 1 or rate > 1 << 12: From 4df880faf6b6f4fc33464580efc052b5969d8aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 8 Sep 2022 08:38:26 +0200 Subject: [PATCH 28/72] clean up docs --- artiq/coredevice/phaser.py | 55 ++++++++++++-------------------------- 1 file changed, 17 insertions(+), 38 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 4b3741344..fc61468d9 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -1292,9 +1292,6 @@ class Miqro: Notes ----- - * The `_mu` suffix in method names refers to parameters and data in "machine units", - i.e. integers. Conversion methods and wrappers to convert from SI units (Hz frequency, - full scale amplitude, turns phase, seconds time) are provided. * The annotation that some operation is "expensive" does not mean it is impossible, just that it may take a significant amount of time and resources to execute such that it may be impractical when used often or during fast pulse sequences. @@ -1324,9 +1321,10 @@ class Miqro: phases, previous profiles, or oscillator history). It is "absolute" in the sense that frequency f and phase p fully determine oscillator output phase p' at time t. This is unlike typical DDS behavior. - * Frequency, phase and amplitude of each oscillator are configurable by selecting one of + * Frequency, phase, and amplitude of each oscillator are configurable by selecting one of n_profile = 32 profiles 0..n_profile-1. This selection is fast and can be done for - each pulse. + each pulse. The phase coherence defined above is guaranteed for each + profile individually. * Note: one profile per oscillator (usually profile index 0) should be reserved for the NOP (no operation, identity) profile, usually with zero amplitude. @@ -1344,51 +1342,38 @@ class Miqro: triggerable shaper. * Triggering the shaper corresponds to passing a pulse from all oscillators to the RF output. - * Any previously staged profiles and phase offsets become active simultaneously + * Selected profiles become active simultaneously (on the same output sample) when triggering the shaper. * The shaper reads (replays) window samples from a memory of size n_window = 1 << 10 starting and stopping at memory locations specified. * Each window memory segment starts with a header determining segment length and interpolation parameters. - * The window samples are interpolated by a factor (rate change) r where log2(r) = 0..n_cic=12 - selectable when triggering. + * The window samples are interpolated by a factor (rate change) + between 1 and r = 1 << 12. * The interpolation order is constant, linear, quadratic, or cubic. This - corresponds to interpolation modes from rectangular window (1st order CIC) - or zero order hold) and to Parzen window (4th order CIC, cubic spline), - selectable when triggering. + corresponds to interpolation modes from rectangular window (1st order CIC) + or zero order hold) and to Parzen window (4th order CIC, cubic spline). * This results in support for pulse lengths of between tau and a bit more than - (1 << 12 + 10) tau ~ 17 ms. + r * n_window * tau = (1 << 12 + 10) tau ~ 17 ms. * Windows can be configured to be head-less and/or tail-less, meaning, they do not feed zero-amplitude samples into the shaper before and after each window. This is used to implement pulses with arbitrary length or CW output. * The window memory can be segmented by choosing different start indices - to support different windows selectable when triggering. - - ### DAC - * This section of the data flow is analogous to the `base` Phaser mode. - * The DAC receives the 250 MS/s I/Q data stream and interpolates it to 1 GS/s I/Q - (with a bandwidth 200 MHz). - * It then applies a (expensive to change) frequency offset of - f1 = -400 MHz..400 MHz. - * Then the DAC converts the data stream to 2 analog outputs I and Q. - * The signals go through two anti-aliasing filters with 340 MHz 3dB bandwidth. - - ### IQ Mixer and PLL (Upconverter variant) - * The analog I and Q signals after the filter are upconverted in a single-sideband IQ - mixer with a f2 = 0.3 GHz..4.8 GHz LO (the "carrier"). - * The output goes through a digitally switchable attenuator (0..31.5 dB attenuation) and - is available at an SMA output with a typical max signal level of 0 to -10 dBm (TBC). + to support different windows. ### Overall properties - * The resulting phase of that signal is + * The DAC may upconvert the signal by applying a frequency offset f1 with + phase p1. + * In the Upconverter Phaser variant, the analog quadrature upconverter + applies another frequency of f2 and phase p2. + * The resulting phase of the signal at the SMA output is (f + f1 + f2)*t + p + s(t - t0) + p1 + p2 (mod 1 turn) - where p1 and p2 are constant but arbitrary and undetermined phase offsets of the - two (common) upconversion stages, s(t - t0) is the phase of the interpolated + where s(t - t0) is the phase of the interpolated shaper output, and t0 is the trigger time (fiducial of the shaper). Unsurprisingly the frequency is the derivative of the phase. * The minimum time to change profiles and phase offsets is ~128 ns (estimate, TBC). - This is the minimum practical pulse interval. + This is the minimum pulse interval. """ def __init__(self, channel): @@ -1396,12 +1381,6 @@ class Miqro: self.base_addr = (self.channel.phaser.channel_base + 1 + self.channel.index) << 8 - @kernel - def write32(self, addr, data): - self.channel.phaser.write16(PHASER_ADDR_MIQRO_MEM_ADDR, - (self.channel.index << 15) | addr) - self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, data) - @kernel def reset(self): for osc in range(16): From af28bf355061d8dd4b51f4e6903160779d45bb14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 8 Sep 2022 08:39:48 +0200 Subject: [PATCH 29/72] simplify dt reset --- artiq/gateware/rtio/phy/phaser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/artiq/gateware/rtio/phy/phaser.py b/artiq/gateware/rtio/phy/phaser.py index d4658e828..3c19d8956 100644 --- a/artiq/gateware/rtio/phy/phaser.py +++ b/artiq/gateware/rtio/phy/phaser.py @@ -111,11 +111,11 @@ class MiqroChannel(Module): dt.eq(dt + 2), ), If(self.ack, - dt.eq(0), + dt[1:].eq(0), + stb.eq(0), If(stb, [r.eq(0) for r in regs], ), - stb.eq(0), ), If(self.rtlink.o.stb, Array(regs)[self.rtlink.o.address].eq(self.rtlink.o.data), From 6085fe3319972ff1679579e30786ccec4848ff0e Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Tue, 13 Sep 2022 09:37:26 +0800 Subject: [PATCH 30/72] experimental-features: add SU Servo coherent phase tracking mode (PR #1467) --- experimental-features/suservo-coherent.diff | 2361 +++++++++++++++++++ 1 file changed, 2361 insertions(+) create mode 100644 experimental-features/suservo-coherent.diff diff --git a/experimental-features/suservo-coherent.diff b/experimental-features/suservo-coherent.diff new file mode 100644 index 000000000..74a528795 --- /dev/null +++ b/experimental-features/suservo-coherent.diff @@ -0,0 +1,2361 @@ +diff --git a/artiq/coredevice/ad9910.py b/artiq/coredevice/ad9910.py +index 801b689ca0..bc19afe25c 100644 +--- a/artiq/coredevice/ad9910.py ++++ b/artiq/coredevice/ad9910.py +@@ -277,6 +277,10 @@ def read32(self, addr: TInt32) -> TInt32: + + :param addr: Register address + """ ++ return self.read32_impl(addr) ++ ++ @kernel ++ def read32_impl(self, addr): + self.bus.set_config_mu(urukul.SPI_CONFIG, 8, + urukul.SPIT_DDS_WR, self.chip_select) + self.bus.write((addr | 0x80) << 24) +@@ -981,7 +985,8 @@ def clear_smp_err(self): + + @kernel + def tune_sync_delay(self, +- search_seed: TInt32 = 15) -> TTuple([TInt32, TInt32]): ++ search_seed: TInt32 = 15, ++ cpld_channel_idx: TInt32 = -1) -> TTuple([TInt32, TInt32]): + """Find a stable SYNC_IN delay. + + This method first locates a valid SYNC_IN delay at zero validation +@@ -997,6 +1002,9 @@ def tune_sync_delay(self, + Defaults to 15 (half range). + :return: Tuple of optimal delay and window size. + """ ++ if cpld_channel_idx == -1: ++ cpld_channel_idx = self.chip_select - 4 ++ assert 0 <= cpld_channel_idx < 4, "Invalid channel index" + if not self.cpld.sync_div: + raise ValueError("parent cpld does not drive SYNC") + search_span = 31 +@@ -1019,7 +1027,7 @@ def tune_sync_delay(self, + delay(100 * us) + err = urukul_sta_smp_err(self.cpld.sta_read()) + delay(100 * us) # slack +- if not (err >> (self.chip_select - 4)) & 1: ++ if not (err >> cpld_channel_idx) & 1: + next_seed = in_delay + break + if next_seed >= 0: # valid delay found, scan next window +diff --git a/artiq/coredevice/suservo.py b/artiq/coredevice/suservo.py +index 1d0a72dad1..f7b516a4e7 100644 +--- a/artiq/coredevice/suservo.py ++++ b/artiq/coredevice/suservo.py +@@ -1,19 +1,19 @@ + from artiq.language.core import kernel, delay, delay_mu, portable + from artiq.language.units import us, ns ++from artiq.language import * + from artiq.coredevice.rtio import rtio_output, rtio_input_data + from artiq.coredevice import spi2 as spi +-from artiq.coredevice import urukul, sampler ++from artiq.coredevice import urukul, sampler, ad9910 ++from math import ceil, log2 ++from numpy import int32, int64 + + +-COEFF_WIDTH = 18 ++COEFF_WIDTH = 18 # Must match gateware IIRWidths.coeff + Y_FULL_SCALE_MU = (1 << (COEFF_WIDTH - 1)) - 1 +-COEFF_DEPTH = 10 + 1 +-WE = 1 << COEFF_DEPTH + 1 +-STATE_SEL = 1 << COEFF_DEPTH +-CONFIG_SEL = 1 << COEFF_DEPTH - 1 +-CONFIG_ADDR = CONFIG_SEL | STATE_SEL + T_CYCLE = (2*(8 + 64) + 2)*8*ns # Must match gateware Servo.t_cycle. +-COEFF_SHIFT = 11 ++COEFF_SHIFT = 11 # Must match gateware IIRWidths.shift ++PROFILE_WIDTH = 5 # Must match gateware IIRWidths.profile ++FINE_TS_WIDTH = 3 # Must match gateware IIRWidths.ioup_dly + + + @portable +@@ -35,21 +35,21 @@ class SUServo: + """Sampler-Urukul Servo parent and configuration device. + + Sampler-Urukul Servo is a integrated device controlling one +- 8-channel ADC (Sampler) and two 4-channel DDS (Urukuls) with a DSP engine +- connecting the ADC data and the DDS output amplitudes to enable ++ 8-channel ADC (Sampler) and any number of 4-channel DDS (Urukuls) with a ++ DSP engine connecting the ADC data and the DDS output amplitudes to enable + feedback. SU Servo can for example be used to implement intensity + stabilization of laser beams with an amplifier and AOM driven by Urukul + and a photodetector connected to Sampler. + + Additionally SU Servo supports multiple preconfigured profiles per channel +- and features like automatic integrator hold. ++ and features like automatic integrator hold and coherent phase tracking. + + Notes: + + * See the SU Servo variant of the Kasli target for an example of how to + connect the gateware and the devices. Sampler and each Urukul need + two EEM connections. +- * Ensure that both Urukuls are AD9910 variants and have the on-board ++ * Ensure that all Urukuls are AD9910 variants and have the on-board + dip switches set to 1100 (first two on, last two off). + * Refer to the Sampler and Urukul documentation and the SU Servo + example device database for runtime configuration of the devices +@@ -65,7 +65,9 @@ class SUServo: + :param core_device: Core device name + """ + kernel_invariants = {"channel", "core", "pgia", "cplds", "ddses", +- "ref_period_mu"} ++ "ref_period_mu", "num_channels", "coeff_sel", ++ "state_sel", "io_dly_addr", "config_addr", ++ "write_enable"} + + def __init__(self, dmgr, channel, pgia_device, + cpld_devices, dds_devices, +@@ -83,9 +85,20 @@ def __init__(self, dmgr, channel, pgia_device, + self.core.coarse_ref_period) + assert self.ref_period_mu == self.core.ref_multiplier + ++ # The width of parts of the servo memory address depends on the number ++ # of channels. ++ self.num_channels = 4 * len(dds_devices) ++ channel_width = ceil(log2(self.num_channels)) ++ coeff_depth = PROFILE_WIDTH + channel_width + 3 ++ self.io_dly_addr = 1 << (coeff_depth - 2) ++ self.state_sel = 2 << (coeff_depth - 2) ++ self.config_addr = 3 << (coeff_depth - 2) ++ self.coeff_sel = 1 << coeff_depth ++ self.write_enable = 1 << (coeff_depth + 1) ++ + @kernel + def init(self): +- """Initialize the servo, Sampler and both Urukuls. ++ """Initialize the servo, Sampler and all Urukuls. + + Leaves the servo disabled (see :meth:`set_config`), resets and + configures all DDS. +@@ -111,8 +124,20 @@ def init(self): + prev_cpld_cfg = cpld.cfg_reg + cpld.cfg_write(prev_cpld_cfg | (0xf << urukul.CFG_MASK_NU)) + dds.init(blind=True) ++ ++ if dds.sync_data.sync_delay_seed != -1: ++ for channel_idx in range(4): ++ mask_nu_this = 1 << (urukul.CFG_MASK_NU + channel_idx) ++ cpld.cfg_write(prev_cpld_cfg | mask_nu_this) ++ delay(8 * us) ++ dds.tune_sync_delay(dds.sync_data.sync_delay_seed, ++ cpld_channel_idx=channel_idx) ++ delay(50 * us) + cpld.cfg_write(prev_cpld_cfg) + ++ self.set_io_update_delays( ++ [dds.sync_data.io_update_delay for dds in self.ddses]) ++ + @kernel + def write(self, addr, value): + """Write to servo memory. +@@ -122,7 +147,7 @@ def write(self, addr, value): + :param addr: Memory location address. + :param value: Data to be written. + """ +- addr |= WE ++ addr |= self.write_enable + value &= (1 << COEFF_WIDTH) - 1 + value |= (addr >> 8) << COEFF_WIDTH + addr = addr & 0xff +@@ -158,7 +183,7 @@ def set_config(self, enable): + Disabling takes up to two servo cycles (~2.3 µs) to clear the + processing pipeline. + """ +- self.write(CONFIG_ADDR, enable) ++ self.write(self.config_addr, enable) + + @kernel + def get_status(self): +@@ -179,7 +204,7 @@ def get_status(self): + :return: Status. Bit 0: enabled, bit 1: done, + bits 8-15: channel clip indicators. + """ +- return self.read(CONFIG_ADDR) ++ return self.read(self.config_addr) + + @kernel + def get_adc_mu(self, adc): +@@ -197,7 +222,8 @@ def get_adc_mu(self, adc): + # State memory entries are 25 bits. Due to the pre-adder dynamic + # range, X0/X1/OFFSET are only 24 bits. Finally, the RTIO interface + # only returns the 18 MSBs (the width of the coefficient memory). +- return self.read(STATE_SEL | (adc << 1) | (1 << 8)) ++ return self.read(self.state_sel | ++ (2 * adc + (1 << PROFILE_WIDTH) * self.num_channels)) + + @kernel + def set_pgia_mu(self, channel, gain): +@@ -236,6 +262,18 @@ def get_adc(self, channel): + gain = (self.gains >> (channel*2)) & 0b11 + return adc_mu_to_volts(val, gain) + ++ @kernel ++ def set_io_update_delays(self, dlys): ++ """Set IO_UPDATE pulse alignment delays. ++ ++ :param dlys: List of delays for each Urukul ++ """ ++ bits = 0 ++ mask_fine_ts = (1 << FINE_TS_WIDTH) - 1 ++ for i in range(len(dlys)): ++ bits |= (dlys[i] & mask_fine_ts) << (FINE_TS_WIDTH * i) ++ self.write(self.io_dly_addr, bits) ++ + + class Channel: + """Sampler-Urukul Servo channel +@@ -256,7 +294,7 @@ def __init__(self, dmgr, channel, servo_device): + self.dds = self.servo.ddses[self.servo_channel // 4] + + @kernel +- def set(self, en_out, en_iir=0, profile=0): ++ def set(self, en_out, en_iir=0, profile=0, en_pt=0): + """Operate channel. + + This method does not advance the timeline. Output RF switch setting +@@ -270,9 +308,26 @@ def set(self, en_out, en_iir=0, profile=0): + :param en_out: RF switch enable + :param en_iir: IIR updates enable + :param profile: Active profile (0-31) ++ :param en_pt: Coherent phase tracking enable ++ * en_pt=1: "coherent phase mode" ++ * en_pt=0: "continuous phase mode" ++ (see :func:`artiq.coredevice.ad9910.AD9910.set_phase_mode` for a ++ definition of the phase modes) + """ + rtio_output(self.channel << 8, +- en_out | (en_iir << 1) | (profile << 2)) ++ en_out | (en_iir << 1) | (en_pt << 2) | (profile << 3)) ++ ++ @kernel ++ def set_reference_time(self): ++ """Set reference time for "coherent phase mode" (see :meth:`set`). ++ ++ This method does not advance the timeline. ++ With en_pt=1 (see :meth:`set`), the tracked DDS output phase of ++ this channel will refer to the current timeline position. ++ ++ """ ++ fine_ts = now_mu() & ((1 << FINE_TS_WIDTH) - 1) ++ rtio_output(self.channel << 8 | 1, self.dds.sysclk_per_mu * fine_ts) + + @kernel + def set_dds_mu(self, profile, ftw, offs, pow_=0): +@@ -285,10 +340,11 @@ def set_dds_mu(self, profile, ftw, offs, pow_=0): + :param offs: IIR offset (17 bit signed) + :param pow_: Phase offset word (16 bit) + """ +- base = (self.servo_channel << 8) | (profile << 3) ++ base = self.servo.coeff_sel | (self.servo_channel << ++ (3 + PROFILE_WIDTH)) | (profile << 3) + self.servo.write(base + 0, ftw >> 16) + self.servo.write(base + 6, (ftw & 0xffff)) +- self.set_dds_offset_mu(profile, offs) ++ self.servo.write(base + 4, offs) + self.servo.write(base + 2, pow_) + + @kernel +@@ -319,7 +375,8 @@ def set_dds_offset_mu(self, profile, offs): + :param profile: Profile number (0-31) + :param offs: IIR offset (17 bit signed) + """ +- base = (self.servo_channel << 8) | (profile << 3) ++ base = self.servo.coeff_sel | (self.servo_channel << ++ (3 + PROFILE_WIDTH)) | (profile << 3) + self.servo.write(base + 4, offs) + + @kernel +@@ -344,6 +401,30 @@ def dds_offset_to_mu(self, offset): + """ + return int(round(offset * (1 << COEFF_WIDTH - 1))) + ++ @kernel ++ def set_dds_phase_mu(self, profile, pow_): ++ """Set only POW in profile DDS coefficients. ++ ++ See :meth:`set_dds_mu` for setting the complete DDS profile. ++ ++ :param profile: Profile number (0-31) ++ :param pow_: Phase offset word (16 bit) ++ """ ++ base = self.servo.coeff_sel | (self.servo_channel << ++ (3 + PROFILE_WIDTH)) | (profile << 3) ++ self.servo.write(base + 2, pow_) ++ ++ @kernel ++ def set_dds_phase(self, profile, phase): ++ """Set only phase in profile DDS coefficients. ++ ++ See :meth:`set_dds` for setting the complete DDS profile. ++ ++ :param profile: Profile number (0-31) ++ :param phase: DDS phase in turns ++ """ ++ self.set_dds_phase_mu(profile, self.dds.turns_to_pow(phase)) ++ + @kernel + def set_iir_mu(self, profile, adc, a1, b0, b1, dly=0): + """Set profile IIR coefficients in machine units. +@@ -378,7 +459,8 @@ def set_iir_mu(self, profile, adc, a1, b0, b1, dly=0): + :param dly: IIR update suppression time. In units of IIR cycles + (~1.2 µs, 0-255). + """ +- base = (self.servo_channel << 8) | (profile << 3) ++ base = self.servo.coeff_sel | (self.servo_channel << ++ (3 + PROFILE_WIDTH)) | (profile << 3) + self.servo.write(base + 3, adc | (dly << 8)) + self.servo.write(base + 1, b1) + self.servo.write(base + 5, a1) +@@ -470,7 +552,9 @@ def get_profile_mu(self, profile, data): + :param profile: Profile number (0-31) + :param data: List of 8 integers to write the profile data into + """ +- base = (self.servo_channel << 8) | (profile << 3) ++ assert len(data) == 8 ++ base = self.servo.coeff_sel | (self.servo_channel << ++ (3 + PROFILE_WIDTH)) | (profile << 3) + for i in range(len(data)): + data[i] = self.servo.read(base + i) + delay(4*us) +@@ -491,7 +575,8 @@ def get_y_mu(self, profile): + :param profile: Profile number (0-31) + :return: 17 bit unsigned Y0 + """ +- return self.servo.read(STATE_SEL | (self.servo_channel << 5) | profile) ++ return self.servo.read(self.servo.state_sel | ( ++ self.servo_channel << PROFILE_WIDTH) | profile) + + @kernel + def get_y(self, profile): +@@ -529,7 +614,8 @@ def set_y_mu(self, profile, y): + """ + # State memory is 25 bits wide and signed. + # Reads interact with the 18 MSBs (coefficient memory width) +- self.servo.write(STATE_SEL | (self.servo_channel << 5) | profile, y) ++ self.servo.write(self.servo.state_sel | ( ++ self.servo_channel << PROFILE_WIDTH) | profile, y) + + @kernel + def set_y(self, profile, y): +@@ -552,3 +638,217 @@ def set_y(self, profile, y): + raise ValueError("Invalid SUServo y-value!") + self.set_y_mu(profile, y_mu) + return y_mu ++ ++ ++class CPLD(urukul.CPLD): ++ """ ++ This module contains a subclass of the Urukul driver class in artiq.coredevice ++ adapted to use CPLD read-back via half-duplex SPI. Only the 8 LSBs can be read ++ back as the read-back buffer on the CPLD is 8 bits wide. ++ """ ++ ++ def __init__(self, dmgr, spi_device, io_update_device=None, ++ **kwargs): ++ # Separate IO_UPDATE TTL output device used by SUServo core, ++ # if active, else by artiq.coredevice.suservo.AD9910 ++ # :meth:`measure_io_update_alignment`. ++ # The urukul.CPLD driver utilises the CPLD CFG register ++ # option instead for pulsing IO_UPDATE of masked DDSs. ++ self.io_update_ttl = dmgr.get(io_update_device) ++ urukul.CPLD.__init__(self, dmgr, spi_device, **kwargs) ++ ++ @kernel ++ def enable_readback(self): ++ """ ++ This method sets the RB_EN flag in the Urukul CPLD configuration ++ register. Once set, the CPLD expects an alternating sequence of ++ two SPI transactions: ++ ++ * 1: Any transaction. If returning data, the 8 LSBs ++ of that will be stored in the CPLD. ++ ++ * 2: One read transaction in half-duplex SPI mode shifting ++ out data from the CPLD over MOSI (use :meth:`readback`). ++ ++ To end this protocol, call :meth:`disable_readback` during step 1. ++ """ ++ self.cfg_write(self.cfg_reg | (1 << urukul.CFG_RB_EN)) ++ ++ @kernel ++ def disable_readback(self): ++ """ ++ This method clears the RB_EN flag in the Urukul CPLD configuration ++ register. This marks the end of the readback protocol (see ++ :meth:`enable_readback`). ++ """ ++ self.cfg_write(self.cfg_reg & ~(1 << urukul.CFG_RB_EN)) ++ ++ @kernel ++ def sta_read(self, full=False): ++ """ ++ Read from status register ++ ++ :param full: retrieve status register by concatenating data from ++ several readback transactions. ++ """ ++ self.enable_readback() ++ self.sta_read_impl() ++ delay(16 * us) # slack ++ r = self.readback() << urukul.STA_RF_SW ++ delay(16 * us) # slack ++ if full: ++ self.enable_readback() # dummy write ++ r |= self.readback(urukul.CS_RB_PLL_LOCK) << urukul.STA_PLL_LOCK ++ delay(16 * us) # slack ++ self.enable_readback() # dummy write ++ r |= self.readback(urukul.CS_RB_PROTO_REV) << urukul.STA_PROTO_REV ++ delay(16 * us) # slack ++ self.disable_readback() ++ return r ++ ++ @kernel ++ def proto_rev_read(self): ++ """Read 8 LSBs of proto_rev""" ++ self.enable_readback() ++ self.enable_readback() # dummy write ++ r = self.readback(urukul.CS_RB_PROTO_REV) ++ self.disable_readback() ++ return r ++ ++ @kernel ++ def pll_lock_read(self): ++ """Read PLL lock status""" ++ self.enable_readback() ++ self.enable_readback() # dummy write ++ r = self.readback(urukul.CS_RB_PLL_LOCK) ++ self.disable_readback() ++ return r & 0xf ++ ++ @kernel ++ def get_att_mu(self): ++ # Different behaviour to urukul.CPLD.get_att_mu: Here, the ++ # latch enable of the attenuators activates 31.5dB ++ # attenuation during the transactions. ++ att_reg = int32(0) ++ self.enable_readback() ++ for i in range(4): ++ self.core.break_realtime() ++ self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 8, ++ urukul.SPIT_ATT_RD, urukul.CS_ATT) ++ self.bus.write(0) # shift in zeros, shift out next 8 bits ++ r = self.readback() & 0xff ++ att_reg |= r << (8 * i) ++ ++ delay(16 * us) # slack ++ self.disable_readback() ++ ++ self.att_reg = int32(att_reg) ++ delay(8 * us) # slack ++ self.set_all_att_mu(self.att_reg) # shift and latch current value again ++ return self.att_reg ++ ++ @kernel ++ def readback(self, cs=urukul.CS_RB_LSBS): ++ """Read from the readback register in half-duplex SPI mode ++ See :meth:`enable_readback` for usage instructions. ++ ++ :param cs: Select data to be returned from the readback register. ++ - urukul.CS_RB_LSBS does not modify the readback register upon readback ++ - urukul.CS_RB_PROTO_REV loads the 8 LSBs of proto_rev ++ - urukul.CS_PLL_LOCK loads the PLL lock status bits concatenated with the ++ IFC mode bits ++ :return: CPLD readback register. ++ """ ++ self.bus.set_config_mu( ++ urukul.SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT | spi.SPI_HALF_DUPLEX, ++ 8, urukul.SPIT_CFG_RD, cs) ++ self.bus.write(0) ++ return int32(self.bus.read()) ++ ++ ++class AD9910(ad9910.AD9910): ++ """ ++ This module contains a subclass of the AD9910 driver class in artiq.coredevice ++ using CPLD read-back via half-duplex SPI. ++ """ ++ ++ # Re-declare set of kernel invariants to avoid warning about non-existent ++ # `sw` attribute, as the AD9910 (instance) constructor writes to the ++ # class attributes. ++ kernel_invariants = { ++ "chip_select", "cpld", "core", "bus", "ftw_per_hz", "sysclk_per_mu" ++ } ++ ++ @kernel ++ def read32(self, addr): ++ """ Read from a 32-bit register ++ ++ This method returns only the 8 LSBs of the return value. ++ """ ++ self.cpld.enable_readback() ++ self.read32_impl(addr) ++ delay(12 * us) # slack ++ r = self.cpld.readback() ++ delay(12 * us) # slack ++ self.cpld.disable_readback() ++ return r ++ ++ @kernel ++ def read64(self, addr): ++ # 3-wire SPI transactions consisting of multiple transfers are not supported. ++ raise NotImplementedError ++ ++ @kernel ++ def read_ram(self, data): ++ # 3-wire SPI transactions consisting of multiple transfers are not supported. ++ raise NotImplementedError ++ ++ @kernel ++ def measure_io_update_alignment(self, delay_start, delay_stop): ++ """Use the digital ramp generator to locate the alignment between ++ IO_UPDATE and SYNC_CLK. ++ ++ Refer to `artiq.coredevice.ad9910` :meth:`measure_io_update_alignment`. ++ In order that this method can operate the io_update_ttl also used by the SUServo ++ core, deactivate the servo before (see :meth:`set_config`). ++ """ ++ # set up DRG ++ self.set_cfr1(drg_load_lrr=1, drg_autoclear=1) ++ # DRG -> FTW, DRG enable ++ self.set_cfr2(drg_enable=1) ++ # no limits ++ self.write64(ad9910._AD9910_REG_RAMP_LIMIT, -1, 0) ++ # DRCTL=0, dt=1 t_SYNC_CLK ++ self.write32(ad9910._AD9910_REG_RAMP_RATE, 0x00010000) ++ # dFTW = 1, (work around negative slope) ++ self.write64(ad9910._AD9910_REG_RAMP_STEP, -1, 0) ++ # un-mask DDS ++ cfg_masked = self.cpld.cfg_reg ++ self.cpld.cfg_write(cfg_masked & ~(0xf << urukul.CFG_MASK_NU)) ++ delay(70 * us) # slack ++ # delay io_update after RTIO edge ++ t = now_mu() + 8 & ~7 ++ at_mu(t + delay_start) ++ # assumes a maximum t_SYNC_CLK period ++ self.cpld.io_update_ttl.pulse(self.core.mu_to_seconds(16 - delay_start)) # realign ++ # re-mask DDS ++ self.cpld.cfg_write(cfg_masked) ++ delay(10 * us) # slack ++ # disable DRG autoclear and LRR on io_update ++ self.set_cfr1() ++ delay(10 * us) # slack ++ # stop DRG ++ self.write64(ad9910._AD9910_REG_RAMP_STEP, 0, 0) ++ delay(10 * us) # slack ++ # un-mask DDS ++ self.cpld.cfg_write(cfg_masked & ~(0xf << urukul.CFG_MASK_NU)) ++ at_mu(t + 0x20000 + delay_stop) ++ self.cpld.io_update_ttl.pulse_mu(16 - delay_stop) # realign ++ # re-mask DDS ++ self.cpld.cfg_write(cfg_masked) ++ ftw = self.read32(ad9910._AD9910_REG_FTW) # read out effective FTW ++ delay(100 * us) # slack ++ # disable DRG ++ self.set_cfr2(drg_enable=0) ++ self.cpld.io_update.pulse_mu(16) ++ return ftw & 1 +diff --git a/artiq/coredevice/urukul.py b/artiq/coredevice/urukul.py +index 2fd66bd65e..61fd476280 100644 +--- a/artiq/coredevice/urukul.py ++++ b/artiq/coredevice/urukul.py +@@ -24,6 +24,7 @@ + CFG_RF_SW = 0 + CFG_LED = 4 + CFG_PROFILE = 8 ++CFG_RB_EN = 11 + CFG_IO_UPDATE = 12 + CFG_MASK_NU = 13 + CFG_CLK_SEL0 = 17 +@@ -51,18 +52,23 @@ + CS_DDS_CH1 = 5 + CS_DDS_CH2 = 6 + CS_DDS_CH3 = 7 ++# chip selects for readback ++CS_RB_PROTO_REV = 1 ++CS_RB_PLL_LOCK = 2 ++CS_RB_LSBS = 3 + + # Default profile + DEFAULT_PROFILE = 7 + + + @portable +-def urukul_cfg(rf_sw, led, profile, io_update, mask_nu, ++def urukul_cfg(rf_sw, led, profile, rb_en, io_update, mask_nu, + clk_sel, sync_sel, rst, io_rst, clk_div): + """Build Urukul CPLD configuration register""" + return ((rf_sw << CFG_RF_SW) | + (led << CFG_LED) | + (profile << CFG_PROFILE) | ++ (rb_en << CFG_RB_EN) | + (io_update << CFG_IO_UPDATE) | + (mask_nu << CFG_MASK_NU) | + ((clk_sel & 0x01) << CFG_CLK_SEL0) | +@@ -191,7 +197,7 @@ def __init__(self, dmgr, spi_device, io_update_device=None, + assert sync_div is None + sync_div = 0 + +- self.cfg_reg = urukul_cfg(rf_sw=rf_sw, led=0, profile=DEFAULT_PROFILE, ++ self.cfg_reg = urukul_cfg(rf_sw=rf_sw, led=0, profile=DEFAULT_PROFILE, rb_en=0, + io_update=0, mask_nu=0, clk_sel=clk_sel, + sync_sel=sync_sel, + rst=0, io_rst=0, clk_div=clk_div) +@@ -226,6 +232,10 @@ def sta_read(self) -> TInt32: + + :return: The status register value. + """ ++ return self.sta_read_impl() ++ ++ @kernel ++ def sta_read_impl(self): + self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, 24, + SPIT_CFG_RD, CS_CFG) + self.bus.write(self.cfg_reg << 8) +diff --git a/artiq/examples/kasli_suservo/device_db.py b/artiq/examples/kasli_suservo/device_db.py +index c52b82a947..8e9d875205 100644 +--- a/artiq/examples/kasli_suservo/device_db.py ++++ b/artiq/examples/kasli_suservo/device_db.py +@@ -142,53 +142,66 @@ + "arguments": {"channel": 15}, + }, + ++ "ttl_urukul0_io_update": { ++ "type": "local", ++ "module": "artiq.coredevice.ttl", ++ "class": "TTLOut", ++ "arguments": {"channel": 16} ++ }, ++ "ttl_urukul1_io_update": { ++ "type": "local", ++ "module": "artiq.coredevice.ttl", ++ "class": "TTLOut", ++ "arguments": {"channel": 17} ++ }, ++ + "suservo0_ch0": { + "type": "local", + "module": "artiq.coredevice.suservo", + "class": "Channel", +- "arguments": {"channel": 16, "servo_device": "suservo0"} ++ "arguments": {"channel": 18, "servo_device": "suservo0"} + }, + "suservo0_ch1": { + "type": "local", + "module": "artiq.coredevice.suservo", + "class": "Channel", +- "arguments": {"channel": 17, "servo_device": "suservo0"} ++ "arguments": {"channel": 19, "servo_device": "suservo0"} + }, + "suservo0_ch2": { + "type": "local", + "module": "artiq.coredevice.suservo", + "class": "Channel", +- "arguments": {"channel": 18, "servo_device": "suservo0"} ++ "arguments": {"channel": 20, "servo_device": "suservo0"} + }, + "suservo0_ch3": { + "type": "local", + "module": "artiq.coredevice.suservo", + "class": "Channel", +- "arguments": {"channel": 19, "servo_device": "suservo0"} ++ "arguments": {"channel": 21, "servo_device": "suservo0"} + }, + "suservo0_ch4": { + "type": "local", + "module": "artiq.coredevice.suservo", + "class": "Channel", +- "arguments": {"channel": 20, "servo_device": "suservo0"} ++ "arguments": {"channel": 22, "servo_device": "suservo0"} + }, + "suservo0_ch5": { + "type": "local", + "module": "artiq.coredevice.suservo", + "class": "Channel", +- "arguments": {"channel": 21, "servo_device": "suservo0"} ++ "arguments": {"channel": 23, "servo_device": "suservo0"} + }, + "suservo0_ch6": { + "type": "local", + "module": "artiq.coredevice.suservo", + "class": "Channel", +- "arguments": {"channel": 22, "servo_device": "suservo0"} ++ "arguments": {"channel": 24, "servo_device": "suservo0"} + }, + "suservo0_ch7": { + "type": "local", + "module": "artiq.coredevice.suservo", + "class": "Channel", +- "arguments": {"channel": 23, "servo_device": "suservo0"} ++ "arguments": {"channel": 25, "servo_device": "suservo0"} + }, + + "suservo0": { +@@ -196,7 +209,7 @@ + "module": "artiq.coredevice.suservo", + "class": "SUServo", + "arguments": { +- "channel": 24, ++ "channel": 26, + "pgia_device": "spi_sampler0_pgia", + "cpld_devices": ["urukul0_cpld", "urukul1_cpld"], + "dds_devices": ["urukul0_dds", "urukul1_dds"], +@@ -207,33 +220,37 @@ + "type": "local", + "module": "artiq.coredevice.spi2", + "class": "SPIMaster", +- "arguments": {"channel": 25} ++ "arguments": {"channel": 27} + }, + + "spi_urukul0": { + "type": "local", + "module": "artiq.coredevice.spi2", + "class": "SPIMaster", +- "arguments": {"channel": 26} ++ "arguments": {"channel": 28} + }, + "urukul0_cpld": { + "type": "local", +- "module": "artiq.coredevice.urukul", ++ "module": "artiq.coredevice.suservo", + "class": "CPLD", + "arguments": { + "spi_device": "spi_urukul0", ++ "io_update_device": "ttl_urukul0_io_update", ++ "sync_device": "clkgen_dds_sync_in", + "refclk": 100e6, + "clk_sel": 0 + } + }, + "urukul0_dds": { + "type": "local", +- "module": "artiq.coredevice.ad9910", ++ "module": "artiq.coredevice.suservo", + "class": "AD9910", + "arguments": { + "pll_n": 40, + "chip_select": 3, + "cpld_device": "urukul0_cpld", ++ "io_update_delay": 0, ++ "sync_delay_seed": -1, + } + }, + +@@ -241,26 +258,40 @@ + "type": "local", + "module": "artiq.coredevice.spi2", + "class": "SPIMaster", +- "arguments": {"channel": 27} ++ "arguments": {"channel": 29} + }, + "urukul1_cpld": { + "type": "local", +- "module": "artiq.coredevice.urukul", ++ "module": "artiq.coredevice.suservo", + "class": "CPLD", + "arguments": { + "spi_device": "spi_urukul1", ++ "io_update_device": "ttl_urukul1_io_update", ++ "sync_device": "clkgen_dds_sync_in", + "refclk": 100e6, + "clk_sel": 0 + } + }, + "urukul1_dds": { + "type": "local", +- "module": "artiq.coredevice.ad9910", ++ "module": "artiq.coredevice.suservo", + "class": "AD9910", + "arguments": { + "pll_n": 40, + "chip_select": 3, + "cpld_device": "urukul1_cpld", ++ "io_update_delay": 0, ++ "sync_delay_seed": -1, ++ } ++ }, ++ ++ "clkgen_dds_sync_in": { ++ "type": "local", ++ "module": "artiq.coredevice.ttl", ++ "class": "TTLClockGen", ++ "arguments": { ++ "channel": 30, ++ "acc_width": 4 + } + }, + +diff --git a/artiq/frontend/artiq_ddb_template.py b/artiq/frontend/artiq_ddb_template.py +index b6d9294a37..93a74d46e7 100755 +--- a/artiq/frontend/artiq_ddb_template.py ++++ b/artiq/frontend/artiq_ddb_template.py +@@ -424,6 +424,16 @@ def process_suservo(self, rtio_offset, peripheral): + sampler_name = self.get_name("sampler") + urukul_names = [self.get_name("urukul") for _ in range(2)] + channel = count(0) ++ for urukul_name in urukul_names: ++ self.gen(""" ++ device_db["ttl_{urukul_name}_io_update"] = {{ ++ "type": "local", ++ "module": "artiq.coredevice.ttl", ++ "class": "TTLOut", ++ "arguments": {{"channel": 0x{ttl_channel:06x}}} ++ }}""", ++ urukul_name=urukul_name, ++ ttl_channel=rtio_offset+next(channel)) + for i in range(8): + self.gen(""" + device_db["{suservo_name}_ch{suservo_chn}"] = {{ +@@ -472,17 +482,19 @@ def process_suservo(self, rtio_offset, peripheral): + }} + device_db["{urukul_name}_cpld"] = {{ + "type": "local", +- "module": "artiq.coredevice.urukul", ++ "module": "artiq.coredevice.suservo", + "class": "CPLD", + "arguments": {{ + "spi_device": "spi_{urukul_name}", ++ "io_update_device": "ttl_{urukul_name}_io_update", ++ "sync_device": "clkgen_{suservo_name}_dds_sync_in", + "refclk": {refclk}, + "clk_sel": {clk_sel} + }} + }} + device_db["{urukul_name}_dds"] = {{ + "type": "local", +- "module": "artiq.coredevice.ad9910", ++ "module": "artiq.coredevice.suservo", + "class": "AD9910", + "arguments": {{ + "pll_n": {pll_n}, +@@ -490,12 +502,25 @@ def process_suservo(self, rtio_offset, peripheral): + "cpld_device": "{urukul_name}_cpld"{pll_vco} + }} + }}""", ++ suservo_name=suservo_name, + urukul_name=urukul_name, + urukul_channel=rtio_offset+next(channel), + refclk=peripheral.get("refclk", self.master_description["rtio_frequency"]), + clk_sel=peripheral["clk_sel"], + pll_vco=",\n \"pll_vco\": {}".format(pll_vco) if pll_vco is not None else "", + pll_n=peripheral["pll_n"]) ++ self.gen(""" ++ device_db["clkgen_{suservo_name}_dds_sync_in"] = {{ ++ "type": "local", ++ "module": "artiq.coredevice.ttl", ++ "class": "TTLClockGen", ++ "arguments": {{ ++ "channel": 0x{clkgen_channel:06x}, ++ "acc_width": 4 ++ }} ++ }}""", ++ suservo_name=suservo_name, ++ clkgen_channel=rtio_offset+next(channel)) + return next(channel) + + def process_zotino(self, rtio_offset, peripheral): +diff --git a/artiq/gateware/eem.py b/artiq/gateware/eem.py +index 467f3cae2e..c7ce7c5879 100644 +--- a/artiq/gateware/eem.py ++++ b/artiq/gateware/eem.py +@@ -6,6 +6,7 @@ + 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 ++from artiq.gateware.rtio.phy import ttl_simple + + + def _eem_signal(i): +@@ -536,17 +537,17 @@ def add_std(cls, target, eem, eem_aux=None, eem_aux2=None, ttl_out_cls=None, + class SUServo(_EEM): + @staticmethod + def io(*eems, iostandard): +- assert len(eems) in (4, 6) +- io = (Sampler.io(*eems[0:2], iostandard=iostandard) +- + Urukul.io_qspi(*eems[2:4], iostandard=iostandard)) +- if len(eems) == 6: # two Urukuls +- io += Urukul.io_qspi(*eems[4:6], iostandard=iostandard) ++ assert len(eems) >= 4 and len(eems) % 2 == 0 ++ io = Sampler.io(*eems[0:2], iostandard=iostandard) ++ for i in range(len(eems) // 2 - 1): ++ io += Urukul.io_qspi(*eems[(2 * i + 2):(2 * i + 4)], iostandard=iostandard) + return io + + @classmethod + def add_std(cls, target, eems_sampler, eems_urukul, + t_rtt=4, clk=1, shift=11, profile=5, +- iostandard=default_iostandard): ++ sync_gen_cls=ttl_simple.ClockGen, ++ iostandard=default_iostandard, sysclk_per_clk=8): + """Add a 8-channel Sampler-Urukul Servo + + :param t_rtt: upper estimate for clock round-trip propagation time from +@@ -562,6 +563,8 @@ def add_std(cls, target, eems_sampler, eems_urukul, + (default: 11) + :param profile: log2 of the number of profiles for each DDS channel + (default: 5) ++ :param sysclk_per_clk: DDS "sysclk" (4*refclk = 1GHz typ.) cycles per ++ FPGA "sys" clock (125MHz typ.) cycles (default: 8) + """ + cls.add_extension( + target, *(eems_sampler + sum(eems_urukul, [])), +@@ -573,27 +576,29 @@ def add_std(cls, target, eems_sampler, eems_urukul, + urukul_pads = servo_pads.UrukulPads( + target.platform, *eem_urukul) + target.submodules += sampler_pads, urukul_pads ++ target.rtio_channels.extend( ++ rtio.Channel.from_phy(phy) for phy in urukul_pads.io_update_phys) + # timings in units of RTIO coarse period + adc_p = servo.ADCParams(width=16, channels=8, lanes=4, t_cnvh=4, + # account for SCK DDR to CONV latency + # difference (4 cycles measured) + t_conv=57 - 4, t_rtt=t_rtt + 4) + iir_p = servo.IIRWidths(state=25, coeff=18, adc=16, asf=14, word=16, +- accu=48, shift=shift, channel=3, +- profile=profile, dly=8) +- dds_p = servo.DDSParams(width=8 + 32 + 16 + 16, +- channels=adc_p.channels, clk=clk) ++ accu=48, shift=shift, profile=profile, dly=8) ++ dds_p = servo.DDSParams(width=8 + 32 + 16 + 16, sysclk_per_clk=sysclk_per_clk, ++ channels=4*len(eem_urukul), clk=clk) + su = servo.Servo(sampler_pads, urukul_pads, adc_p, iir_p, dds_p) + su = ClockDomainsRenamer("rio_phy")(su) + # explicitly name the servo submodule to enable the migen namer to derive + # a name for the adc return clock domain + setattr(target.submodules, "suservo_eem{}".format(eems_sampler[0]), su) + +- ctrls = [rtservo.RTServoCtrl(ctrl) for ctrl in su.iir.ctrl] ++ ctrls = [rtservo.RTServoCtrl(ctrl, ctrl_reftime) ++ for ctrl, ctrl_reftime in zip(su.iir.ctrl, su.iir.ctrl_reftime)] + target.submodules += ctrls + target.rtio_channels.extend( + rtio.Channel.from_phy(ctrl) for ctrl in ctrls) +- mem = rtservo.RTServoMem(iir_p, su) ++ mem = rtservo.RTServoMem(iir_p, su, urukul_pads.io_update_phys) + target.submodules += mem + target.rtio_channels.append(rtio.Channel.from_phy(mem, ififo_depth=4)) + +@@ -603,27 +608,24 @@ def add_std(cls, target, eems_sampler, eems_urukul, + target.submodules += phy + target.rtio_channels.append(rtio.Channel.from_phy(phy, ififo_depth=4)) + +- for i in range(2): +- if len(eem_urukul) > i: +- spi_p, spi_n = ( +- target.platform.request("{}_spi_p".format(eem_urukul[i])), +- target.platform.request("{}_spi_n".format(eem_urukul[i]))) +- else: # create a dummy bus +- spi_p = Record([("clk", 1), ("cs_n", 1)]) # mosi, cs_n +- spi_n = None +- ++ for eem_urukuli in eem_urukul: ++ spi_p, spi_n = ( ++ target.platform.request("{}_spi_p".format(eem_urukuli)), ++ target.platform.request("{}_spi_n".format(eem_urukuli))) + phy = spi2.SPIMaster(spi_p, spi_n) + target.submodules += phy + target.rtio_channels.append(rtio.Channel.from_phy(phy, ififo_depth=4)) + +- for j, eem_urukuli in enumerate(eem_urukul): +- pads = target.platform.request("{}_dds_reset_sync_in".format(eem_urukuli)) +- target.specials += DifferentialOutput(0, pads.p, pads.n) ++ if sync_gen_cls is not None: # AD9910 variant and SYNC_IN from EEM ++ phy = sync_gen_cls(urukul_pads.dds_reset_sync_in, ftw_width=4) ++ target.submodules += phy ++ target.rtio_channels.append(rtio.Channel.from_phy(phy)) + ++ for j, eem_urukuli in enumerate(eem_urukul): + for i, signal in enumerate("sw0 sw1 sw2 sw3".split()): + pads = target.platform.request("{}_{}".format(eem_urukuli, signal)) + target.specials += DifferentialOutput( +- su.iir.ctrl[j*4 + i].en_out, pads.p, pads.n) ++ su.iir.ctrl[j * 4 + i].en_out, pads.p, pads.n) + + + class Mirny(_EEM): +diff --git a/artiq/gateware/rtio/phy/servo.py b/artiq/gateware/rtio/phy/servo.py +index 9fa6345211..0f7ebbf4b2 100644 +--- a/artiq/gateware/rtio/phy/servo.py ++++ b/artiq/gateware/rtio/phy/servo.py +@@ -1,25 +1,32 @@ + from migen import * +- + from artiq.gateware.rtio import rtlink + + + class RTServoCtrl(Module): + """Per channel RTIO control interface""" +- def __init__(self, ctrl): ++ def __init__(self, ctrl, ctrl_reftime): + self.rtlink = rtlink.Interface( +- rtlink.OInterface(len(ctrl.profile) + 2)) ++ rtlink.OInterface( ++ data_width=max(len(ctrl.profile) + 3, ++ len(ctrl_reftime.sysclks_fine)), ++ address_width=1) ++ ) + + # # # + ++ sel_ref = self.rtlink.o.address[0] + self.comb += [ +- ctrl.stb.eq(self.rtlink.o.stb), +- self.rtlink.o.busy.eq(0) ++ ctrl.stb.eq(self.rtlink.o.stb & ~sel_ref), ++ self.rtlink.o.busy.eq(0), ++ ctrl_reftime.stb.eq(self.rtlink.o.stb & sel_ref), + ] ++ ctrl_cases = { ++ 0: Cat(ctrl.en_out, ctrl.en_iir, ctrl.en_pt, ctrl.profile).eq( ++ self.rtlink.o.data), ++ 1: ctrl_reftime.sysclks_fine.eq(self.rtlink.o.data), ++ } + self.sync.rio_phy += [ +- If(self.rtlink.o.stb, +- Cat(ctrl.en_out, ctrl.en_iir, ctrl.profile).eq( +- self.rtlink.o.data) +- ) ++ If(self.rtlink.o.stb, Case(self.rtlink.o.address, ctrl_cases)) + ] + + +@@ -34,35 +41,45 @@ class RTServoMem(Module): + """All-channel all-profile coefficient and state RTIO control + interface. + ++ The real-time interface exposes the following functions: ++ 1. enable/disable servo iterations ++ 2. read the servo status (including state of clip register) ++ 3. access the IIR coefficient memory (set PI loop gains etc.) ++ 4. access the IIR state memory (set offset and read ADC data) ++ ++ The bit assignments for the servo address space are (from MSB): ++ * write-enable (1 bit) ++ * sel_coeff (1 bit) ++ If selected, the coefficient memory location is ++ addressed by all the lower bits excluding the LSB (high_coeff). ++ - high_coeff (1 bit) selects between the upper and lower halves of that ++ memory location. ++ Else (if ~sel_coeff), the following bits are: ++ - sel (2 bits) selects between the following memory locations: ++ ++ destination | sel | sel_coeff | ++ ----------------|-------|--------------| ++ IIR coeff mem | - | 1 | ++ DDS delay mem | 1 | 0 | ++ IIR state mem | 2 | 0 | ++ config (write) | 3 | 0 | ++ status (read) | 3 | 0 | ++ ++ - IIR state memory address ++ + Servo internal addresses are internal_address_width wide, which is + typically longer than the 8-bit RIO address space. We pack the overflow + onto the RTIO data word after the data. + +- Servo address space (from LSB): +- - IIR coefficient/state memory address, (w.profile + w.channel + 2) bits. +- If the state memory is selected, the lower bits are used directly as +- the memory address. If the coefficient memory is selected, the LSB +- (high_coeff) selects between the upper and lower halves of the memory +- location, which is two coefficients wide, with the remaining bits used +- as the memory address. +- - config_sel (1 bit) +- - state_sel (1 bit) +- - we (1 bit) +- +- destination | config_sel | state_sel +- ----------------|------------|---------- +- IIR coeff mem | 0 | 0 +- IIR coeff mem | 1 | 0 +- IIR state mem | 0 | 1 +- config (write) | 1 | 1 +- status (read) | 1 | 1 ++ The address layout reflects the fact that typically, the coefficient memory ++ address is 2 bits wider than the state memory address. + + Values returned to the user on the Python side of the RTIO interface are + 32 bit, so we sign-extend all values from w.coeff to that width. This works + (instead of having to decide whether to sign- or zero-extend per address), as + all unsigned values are less wide than w.coeff. + """ +- def __init__(self, w, servo): ++ def __init__(self, w, servo, io_update_phys): + m_coeff = servo.iir.m_coeff.get_port(write_capable=True, + mode=READ_FIRST, + we_granularity=w.coeff, clock_domain="rio") +@@ -71,6 +88,7 @@ def __init__(self, w, servo): + # mode=READ_FIRST, + clock_domain="rio") + self.specials += m_state, m_coeff ++ w_channel = bits_for(len(servo.iir.dds) - 1) + + # just expose the w.coeff (18) MSBs of state + assert w.state >= w.coeff +@@ -83,7 +101,7 @@ def __init__(self, w, servo): + assert 8 + w.dly < w.coeff + + # coeff, profile, channel, 2 mems, rw +- internal_address_width = 3 + w.profile + w.channel + 1 + 1 ++ internal_address_width = 3 + w.profile + w_channel + 1 + 1 + rtlink_address_width = min(8, internal_address_width) + overflow_address_width = internal_address_width - rtlink_address_width + self.rtlink = rtlink.Interface( +@@ -99,7 +117,7 @@ def __init__(self, w, servo): + # # # + + config = Signal(w.coeff, reset=0) +- status = Signal(w.coeff) ++ status = Signal(len(self.rtlink.i.data)) + pad = Signal(6) + self.comb += [ + Cat(servo.start).eq(config), +@@ -109,15 +127,19 @@ def __init__(self, w, servo): + + assert len(self.rtlink.o.address) + len(self.rtlink.o.data) - w.coeff == ( + 1 + # we +- 1 + # state_sel ++ 1 + # sel_coeff + 1 + # high_coeff + len(m_coeff.adr)) +- # ensure that we can fit config/status into the state address space ++ # ensure that we can fit config/io_dly/status into the state address space + assert len(self.rtlink.o.address) + len(self.rtlink.o.data) - w.coeff >= ( + 1 + # we +- 1 + # state_sel +- 1 + # config_sel ++ 1 + # sel_coeff ++ 2 + # sel + len(m_state.adr)) ++ # ensure that IIR state mem addresses are at least 2 bits less wide than ++ # IIR coeff mem addresses to ensure we can fit SEL after the state mem ++ # address and before the SEL_COEFF bit. ++ assert w.profile + w_channel >= 4 + + internal_address = Signal(internal_address_width) + self.comb += internal_address.eq(Cat(self.rtlink.o.address, +@@ -127,52 +149,60 @@ def __init__(self, w, servo): + self.comb += coeff_data.eq(self.rtlink.o.data[:w.coeff]) + + we = internal_address[-1] +- state_sel = internal_address[-2] +- config_sel = internal_address[-3] ++ sel_coeff = internal_address[-2] ++ sel1 = internal_address[-3] ++ sel0 = internal_address[-4] + high_coeff = internal_address[0] ++ sel = Signal(2) + self.comb += [ + self.rtlink.o.busy.eq(0), ++ sel.eq(Mux(sel_coeff, 0, Cat(sel0, sel1))), + m_coeff.adr.eq(internal_address[1:]), + m_coeff.dat_w.eq(Cat(coeff_data, coeff_data)), +- m_coeff.we[0].eq(self.rtlink.o.stb & ~high_coeff & +- we & ~state_sel), +- m_coeff.we[1].eq(self.rtlink.o.stb & high_coeff & +- we & ~state_sel), ++ m_coeff.we[0].eq(self.rtlink.o.stb & ~high_coeff & we & sel_coeff), ++ m_coeff.we[1].eq(self.rtlink.o.stb & high_coeff & we & sel_coeff), + m_state.adr.eq(internal_address), + m_state.dat_w[w.state - w.coeff:].eq(self.rtlink.o.data), +- m_state.we.eq(self.rtlink.o.stb & we & state_sel & ~config_sel), ++ m_state.we.eq(self.rtlink.o.stb & we & (sel == 2)), + ] + read = Signal() +- read_state = Signal() + read_high = Signal() +- read_config = Signal() ++ read_sel = Signal(2) + self.sync.rio += [ + If(read, + read.eq(0) + ), + If(self.rtlink.o.stb, + read.eq(~we), +- read_state.eq(state_sel), ++ read_sel.eq(sel), + read_high.eq(high_coeff), +- read_config.eq(config_sel), + ) + ] ++ ++ # I/O update alignment delays ++ ioup_dlys = Cat(*[phy.fine_ts for phy in io_update_phys]) ++ assert w.coeff >= len(ioup_dlys) ++ + self.sync.rio_phy += [ +- If(self.rtlink.o.stb & we & state_sel & config_sel, ++ If(self.rtlink.o.stb & we & (sel == 3), + config.eq(self.rtlink.o.data) + ), +- If(read & read_config & read_state, ++ If(read & (read_sel == 3), + [_.clip.eq(0) for _ in servo.iir.ctrl] +- ) ++ ), ++ If(self.rtlink.o.stb & we & (sel == 1), ++ ioup_dlys.eq(self.rtlink.o.data) ++ ), + ] ++ ++ # read return value by destination ++ read_acts = Array([ ++ Mux(read_high, m_coeff.dat_r[w.coeff:], m_coeff.dat_r[:w.coeff]), ++ ioup_dlys, ++ m_state.dat_r[w.state - w.coeff:], ++ status ++ ]) + self.comb += [ + self.rtlink.i.stb.eq(read), +- _eq_sign_extend(self.rtlink.i.data, +- Mux(read_state, +- Mux(read_config, +- status, +- m_state.dat_r[w.state - w.coeff:]), +- Mux(read_high, +- m_coeff.dat_r[w.coeff:], +- m_coeff.dat_r[:w.coeff]))) ++ _eq_sign_extend(self.rtlink.i.data, read_acts[read_sel]), + ] +diff --git a/artiq/gateware/suservo/dds_ser.py b/artiq/gateware/suservo/dds_ser.py +index 38d1f6d946..cdccfcc98e 100644 +--- a/artiq/gateware/suservo/dds_ser.py ++++ b/artiq/gateware/suservo/dds_ser.py +@@ -1,4 +1,5 @@ + import logging ++from collections import namedtuple + + from migen import * + +@@ -6,11 +7,11 @@ + + from . import spi + +- + logger = logging.getLogger(__name__) + +- +-DDSParams = spi.SPIParams ++DDSParams = namedtuple("DDSParams", spi.SPIParams._fields + ( ++ "sysclk_per_clk", # DDS_CLK per FPGA system clock ++)) + + + class DDS(spi.SPISimple): +diff --git a/artiq/gateware/suservo/iir.py b/artiq/gateware/suservo/iir.py +index 0ec9bfa093..3fad77a6ea 100644 +--- a/artiq/gateware/suservo/iir.py ++++ b/artiq/gateware/suservo/iir.py +@@ -1,6 +1,7 @@ + from collections import namedtuple + import logging + from migen import * ++from migen.genlib.coding import Encoder + + logger = logging.getLogger(__name__) + +@@ -16,7 +17,6 @@ + "word", # "word" size to break up DDS profile data (16) + "asf", # unsigned amplitude scale factor for DDS (14) + "shift", # fixed point scaling coefficient for a1, b0, b1 (log2!) (11) +- "channel", # channels (log2!) (3) + "profile", # profiles per channel (log2!) (5) + "dly", # the activation delay + ]) +@@ -99,14 +99,14 @@ class IIR(Module): + This module implements a multi-channel IIR (infinite impulse response) + filter processor optimized for synthesis on FPGAs. + +- The module is parametrized by passing a ``IIRWidths()`` object which +- will be abbreviated W here. ++ The module is parametrized by passing a ``IIRWidths()`` object, and ++ two more objects which will be abbreviated W, W_O and W_I here. + +- It reads 1 << W.channels input channels (typically from an ADC) ++ It reads W_I.channels input channels (typically from an ADC) + and on each iteration processes the data using a first-order IIR filter. + At the end of the cycle each the output of the filter together with + additional data (typically frequency tunning word and phase offset word +- for a DDS) are presented at the 1 << W.channels outputs of the module. ++ for a DDS) are presented at the W_O.channels outputs of the module. + + Profile memory + ============== +@@ -145,10 +145,10 @@ class IIR(Module): + ------------- + + The state memory holds all Y1 values (IIR processor outputs) for all +- profiles of all channels in the lower half (1 << W.profile + W.channel +- addresses) and the pairs of old and new ADC input values X1, and X0, +- in the upper half (1 << W.channel addresses). Each memory location is +- W.state bits wide. ++ profiles of all channels in the lower half (1 << W.profile)*W_O.channels ++ addresses, and the pairs of old and new ADC input values X1, and X0, ++ in the upper half (W_I.channels addresses). ++ Each memory location is W.state bits wide. + + Real-time control + ================= +@@ -157,15 +157,16 @@ class IIR(Module): + + * The active profile, PROFILE + * Whether to perform IIR filter iterations, EN_IIR ++ * Whether to track the DDS phase coherently, EN_PT + * The RF switch state enabling output from the channel, EN_OUT + + Delayed IIR processing + ====================== + +- The IIR filter iterations on a given channel are only performed all of the +- following are true: ++ The IIR filter iterations on a given channel are only performed if all of ++ the following are true: + +- * PROFILE, EN_IIR, EN_OUT have not been updated in the within the ++ * PROFILE, EN_IIR, EN_OUT have not been updated within the + last DLY cycles + * EN_IIR is asserted + * EN_OUT is asserted +@@ -176,9 +177,8 @@ class IIR(Module): + Typical design at the DSP level. This does not include the description of + the pipelining or the overall latency involved. + +- IIRWidths(state=25, coeff=18, adc=16, +- asf=14, word=16, accu=48, shift=11, +- channel=3, profile=5, dly=8) ++ IIRWidths(state=25, coeff=18, adc=16, asf=14, ++ word=16, accu=48, shift=11, profile=5, dly=8) + + X0 = ADC * 2^(25 - 1 - 16) + X1 = X0 delayed by one cycle +@@ -213,39 +213,64 @@ class IIR(Module): + --/--: signal with a given bit width always includes a sign bit + -->--: flow is to the right and down unless otherwise indicated + """ +- def __init__(self, w): +- self.widths = w +- for i, j in enumerate(w): +- assert j > 0, (i, j, w) ++ def __init__(self, w, w_i, w_o, t_cycle): ++ for v in (w, w_i, w_o): ++ for i, j in enumerate(v): ++ assert j > 0, (i, j, v) + assert w.word <= w.coeff # same memory + assert w.state + w.coeff + 3 <= w.accu + ++ # Reference counter for coherent phase tracking (we assume this doesn't ++ # roll over – a good assumption, as the period is, for a typical clock ++ # frequency, 2^48 / 125 MHz = ~26 days). ++ self.t_running = Signal(48, reset_less=True) ++ ++ # If true, internal DDS phase tracking state is reset, matching DDS ++ # chips with phase cleared (and zero FTW) before the start of the ++ # iteration. Automatically reset at the end of the iteration. ++ self.reset_dds_phase = Signal() ++ + # m_coeff of active profiles should only be accessed externally during + # ~processing + self.specials.m_coeff = Memory( + width=2*w.coeff, # Cat(pow/ftw/offset, cfg/a/b) +- depth=4 << w.profile + w.channel) ++ depth=(4 << w.profile) * w_o.channels) + # m_state[x] should only be read externally during ~(shifting | loading) + # m_state[y] of active profiles should only be read externally during + # ~processing + self.specials.m_state = Memory( + width=w.state, # y1,x0,x1 +- depth=(1 << w.profile + w.channel) + (2 << w.channel)) ++ depth=(1 << w.profile) * w_o.channels + 2 * w_i.channels) + # ctrl should only be updated synchronously + self.ctrl = [Record([ + ("profile", w.profile), + ("en_out", 1), + ("en_iir", 1), ++ ("en_pt", 1), + ("clip", 1), + ("stb", 1)]) +- for i in range(1 << w.channel)] ++ for i in range(w_o.channels)] ++ # "Shadow copy" of phase accumulator in DDS accumulator for each output ++ # channel. ++ self.specials.m_accum_ftw = Memory( ++ width=2 * w.word, ++ depth=w_o.channels) ++ # ctrl_reftime should only be updated synchronously ++ self.ctrl_reftime = [Record([ ++ ("sysclks_fine", bits_for(w_o.sysclk_per_clk - 1)), ++ ("stb", 1)]) ++ for i in range(w_o.channels)] ++ # Reference time for each output channel. ++ self.specials.m_t_ref = Memory( ++ width=len(self.t_running), ++ depth=w_o.channels) + # only update during ~loading + self.adc = [Signal((w.adc, True), reset_less=True) +- for i in range(1 << w.channel)] ++ for i in range(w_i.channels)] + # Cat(ftw0, ftw1, pow, asf) + # only read externally during ~processing +- self.dds = [Signal(4*w.word, reset_less=True) +- for i in range(1 << w.channel)] ++ self.dds = [Signal(4 * w.word, reset_less=True) ++ for i in range(w_o.channels)] + # perform one IIR iteration, start with loading, + # then processing, then shifting, end with done + self.start = Signal() +@@ -265,8 +290,15 @@ def __init__(self, w): + profiles = Array([ch.profile for ch in self.ctrl]) + en_outs = Array([ch.en_out for ch in self.ctrl]) + en_iirs = Array([ch.en_iir for ch in self.ctrl]) ++ en_pts = Array([ch.en_pt for ch in self.ctrl]) + clips = Array([ch.clip for ch in self.ctrl]) + ++ # Sample of the reference counter at the start of the current iteration, ++ # such that a common reference time is used for phase calculations ++ # across all channels, in DDS sysclk units. ++ sysclks_to_iter_start = Signal( ++ len(self.t_running) + bits_for(w_o.sysclk_per_clk - 1)) ++ + # Main state machine sequencing the steps of each servo iteration. The + # module IDLEs until self.start is asserted, and then runs through LOAD, + # PROCESS and SHIFT in order (see description of corresponding flags +@@ -281,7 +313,7 @@ def __init__(self, w): + # using the (MSBs of) t_current_step, and, after all channels have been + # covered, proceed once the pipeline has completely drained. + self.submodules.fsm = fsm = FSM("IDLE") +- t_current_step = Signal(w.channel + 2) ++ t_current_step = Signal(max=max(4 * (w_o.channels + 2), 2 * w_i.channels)) + t_current_step_clr = Signal() + + # pipeline group activity flags (SR) +@@ -293,12 +325,13 @@ def __init__(self, w): + self.done.eq(1), + t_current_step_clr.eq(1), + If(self.start, ++ NextValue(sysclks_to_iter_start, self.t_running * w_o.sysclk_per_clk), + NextState("LOAD") + ) + ) + fsm.act("LOAD", + self.loading.eq(1), +- If(t_current_step == (1 << w.channel) - 1, ++ If(t_current_step == w_i.channels - 1, + t_current_step_clr.eq(1), + NextValue(stages_active[0], 1), + NextState("PROCESS") +@@ -311,11 +344,12 @@ def __init__(self, w): + If(stages_active == 0, + t_current_step_clr.eq(1), + NextState("SHIFT"), ++ NextValue(self.reset_dds_phase, 0) + ) + ) + fsm.act("SHIFT", + self.shifting.eq(1), +- If(t_current_step == (2 << w.channel) - 1, ++ If(t_current_step == 2 * w_i.channels - 1, + NextState("IDLE") + ) + ) +@@ -333,13 +367,13 @@ def __init__(self, w): + # pipeline group channel pointer (SR) + # for each pipeline stage, this is the channel currently being + # processed +- channel = [Signal(w.channel, reset_less=True) for i in range(3)] ++ channel = [Signal(max=w_o.channels, reset_less=True) for i in range(3)] + self.comb += Cat(pipeline_phase, channel[0]).eq(t_current_step) + self.sync += [ + If(pipeline_phase == 3, + Cat(channel[1:]).eq(Cat(channel[:-1])), + stages_active[1:].eq(stages_active[:-1]), +- If(channel[0] == (1 << w.channel) - 1, ++ If(channel[0] == w_o.channels - 1, + stages_active[0].eq(0) + ) + ) +@@ -393,13 +427,13 @@ def __init__(self, w): + + # selected adc and profile delay (combinatorial from dat_r) + # both share the same coeff word (sel in the lower 8 bits) +- sel_profile = Signal(w.channel) ++ sel_profile = Signal(max=w_i.channels) + dly_profile = Signal(w.dly) +- assert w.channel <= 8 ++ assert w_o.channels < (1 << 8) + assert 8 + w.dly <= w.coeff + + # latched adc selection +- sel = Signal(w.channel, reset_less=True) ++ sel = Signal(max=w_i.channels, reset_less=True) + # iir enable SR + en = Signal(2, reset_less=True) + +@@ -407,12 +441,12 @@ def __init__(self, w): + sel_profile.eq(m_coeff.dat_r[w.coeff:]), + dly_profile.eq(m_coeff.dat_r[w.coeff + 8:]), + If(self.shifting, +- m_state.adr.eq(t_current_step | (1 << w.profile + w.channel)), ++ m_state.adr.eq(t_current_step + (1 << w.profile) * w_o.channels), + m_state.dat_w.eq(m_state.dat_r), + m_state.we.eq(t_current_step[0]) + ), + If(self.loading, +- m_state.adr.eq((t_current_step << 1) | (1 << w.profile + w.channel)), ++ m_state.adr.eq((t_current_step << 1) + (1 << w.profile) * w_o.channels), + m_state.dat_w[-w.adc - 1:-1].eq(Array(self.adc)[t_current_step]), + m_state.dat_w[-1].eq(m_state.dat_w[-2]), + m_state.we.eq(1) +@@ -424,9 +458,9 @@ def __init__(self, w): + # read old y + Cat(profile[0], channel[0]), + # read x0 (recent) +- 0 | (sel_profile << 1) | (1 << w.profile + w.channel), ++ 0 | (sel_profile << 1) + (1 << w.profile) * w_o.channels, + # read x1 (old) +- 1 | (sel << 1) | (1 << w.profile + w.channel), ++ 1 | (sel << 1) + (1 << w.profile) * w_o.channels, + ])[pipeline_phase]), + m_state.dat_w.eq(dsp.output), + m_state.we.eq((pipeline_phase == 0) & stages_active[2] & en[1]), +@@ -438,11 +472,9 @@ def __init__(self, w): + # + + # internal channel delay counters +- dlys = Array([Signal(w.dly) +- for i in range(1 << w.channel)]) +- self._dlys = dlys # expose for debugging only ++ dlys = Array([Signal(w.dly) for i in range(w_o.channels)]) + +- for i in range(1 << w.channel): ++ for i in range(w_o.channels): + self.sync += [ + # (profile != profile_old) | ~en_out + If(self.ctrl[i].stb, +@@ -482,25 +514,81 @@ def __init__(self, w): + }), + ] + ++ # Update coarse reference time from t_running upon ctrl_reftime strobe ++ ref_stb_encoder = Encoder(w_o.channels) ++ m_t_ref_stb = self.m_t_ref.get_port(write_capable=True) ++ self.specials += m_t_ref_stb ++ self.submodules += ref_stb_encoder ++ self.comb += [ ++ ref_stb_encoder.i.eq(Cat([ch.stb for ch in self.ctrl_reftime])), ++ m_t_ref_stb.adr.eq(ref_stb_encoder.o), ++ m_t_ref_stb.we.eq(~ref_stb_encoder.n), ++ m_t_ref_stb.dat_w.eq(self.t_running), ++ ] ++ + # +- # Update DDS profile with FTW/POW/ASF +- # Stage 0 loads the POW, stage 1 the FTW, and stage 2 writes +- # the ASF computed by the IIR filter. ++ # Update DDS profile with FTW/POW/ASF (including phase tracking, if ++ # enabled). Stage 0 loads the POW, stage 1 the FTW, and stage 2 writes ++ # the ASF computed by the IIR filter (and adds any phase correction). + # + + # muxing + ddss = Array(self.dds) ++ sysclks_ref_fine = Array([ch.sysclks_fine for ch in self.ctrl_reftime]) ++ ++ # registered copy of FTW on channel[1] ++ current_ftw = Signal(2 * w.word, reset_less=True) ++ # target effective DDS phase (accumulator + POW) at the coming io_update ++ target_dds_phase = Signal.like(current_ftw) ++ # DDS-internal phase accumulated until the coming io_update ++ accum_dds_phase = Signal.like(current_ftw) ++ # correction to add to the bare POW to yield a phase-coherent DDS output ++ correcting_pow = Signal(w.word, reset_less=True) ++ # sum of all FTWs on channel[1], updated with current FTW during the ++ # calculation ++ accum_ftw = Signal.like(current_ftw) ++ # sum of previous FTWs on channel[1] (or 0 on phase coherence reference ++ # reset) ++ prev_accum_ftw = Signal.like(current_ftw) ++ # time since reference time at coming io_update in DDS sysclk units ++ sysclks_to_ref = Signal.like(sysclks_to_iter_start) ++ # t_ref in DDS sysclk units ++ sysclks_ref_to_iter_start = Signal.like(sysclks_to_iter_start) ++ ++ m_t_ref = self.m_t_ref.get_port() ++ m_accum_ftw = self.m_accum_ftw.get_port(write_capable=True, mode=READ_FIRST) ++ self.specials += m_accum_ftw, m_t_ref ++ prev_accum_ftw = Signal.like(accum_ftw) ++ self.comb += [ ++ prev_accum_ftw.eq(Mux(self.reset_dds_phase, 0, m_accum_ftw.dat_r)), ++ m_accum_ftw.adr.eq(channel[1]), ++ m_accum_ftw.we.eq((pipeline_phase == 3) & stages_active[1]), ++ m_accum_ftw.dat_w.eq(accum_ftw), ++ m_t_ref.adr.eq(channel[0]), ++ ] + ++ sysclks_per_iter = t_cycle * w_o.sysclk_per_clk + self.sync += [ + Case(pipeline_phase, { + 0: [ + If(stages_active[1], + ddss[channel[1]][:w.word].eq(m_coeff.dat_r), # ftw0 ++ current_ftw[:w.word].eq(m_coeff.dat_r), ++ sysclks_ref_to_iter_start.eq(m_t_ref.dat_r * w_o.sysclk_per_clk), ++ ), ++ If(stages_active[2] & en_pts[channel[2]], ++ # add pow correction if phase tracking enabled ++ ddss[channel[2]][2*w.word:3*w.word].eq( ++ ddss[channel[2]][2*w.word:3*w.word] + correcting_pow), + ), + ], + 1: [ + If(stages_active[1], + ddss[channel[1]][w.word:2 * w.word].eq(m_coeff.dat_r), # ftw1 ++ current_ftw[w.word:].eq(m_coeff.dat_r), ++ sysclks_to_ref.eq(sysclks_to_iter_start - ( ++ sysclks_ref_to_iter_start + sysclks_ref_fine[channel[1]])), ++ accum_dds_phase.eq(prev_accum_ftw * sysclks_per_iter), + ), + If(stages_active[2], + ddss[channel[2]][3*w.word:].eq( # asf +@@ -509,14 +597,40 @@ def __init__(self, w): + ], + 2: [ + If(stages_active[0], +- ddss[channel[0]][2*w.word:3*w.word].eq(m_coeff.dat_r), # pow ++ # Load bare POW from profile memory. ++ ddss[channel[0]][2*w.word:3*w.word].eq(m_coeff.dat_r), ++ ), ++ If(stages_active[1], ++ target_dds_phase.eq(current_ftw * sysclks_to_ref), ++ accum_ftw.eq(prev_accum_ftw + current_ftw), + ), + ], + 3: [ ++ If(stages_active[1], ++ # Prepare most-significant word to add to POW from ++ # profile for phase tracking. ++ correcting_pow.eq( ++ (target_dds_phase - accum_dds_phase)[w.word:]), ++ ), + ], + }), + ] + ++ # expose for simulation and debugging only ++ self.widths = w ++ self.widths_adc = w_i ++ self.widths_dds = w_o ++ self.t_cycle = t_cycle ++ self._state = t_current_step ++ self._stages = stages_active ++ self._dt_start = sysclks_to_iter_start ++ self._sysclks_to_ref = sysclks_to_ref ++ self._sysclks_ref_to_iter_start = sysclks_ref_to_iter_start ++ self._sysclks_ref_fine = sysclks_ref_fine ++ self._ph_acc = accum_dds_phase ++ self._ph_coh = target_dds_phase ++ self._dlys = dlys ++ + def _coeff(self, channel, profile, coeff): + """Return ``high_word``, ``address`` and bit ``mask`` for the + storage of coefficient name ``coeff`` in profile ``profile`` +@@ -564,35 +678,45 @@ def get_coeff(self, channel, profile, coeff): + def set_state(self, channel, val, profile=None, coeff="y1"): + """Set a state value.""" + w = self.widths ++ w_o = self.widths_dds + if coeff == "y1": + assert profile is not None + yield self.m_state[profile | (channel << w.profile)].eq(val) + elif coeff == "x0": + assert profile is None +- yield self.m_state[(channel << 1) | +- (1 << w.profile + w.channel)].eq(val) ++ yield self.m_state[(channel << 1) + ++ (1 << w.profile) * w_o.channels].eq(val) + elif coeff == "x1": + assert profile is None +- yield self.m_state[1 | (channel << 1) | +- (1 << w.profile + w.channel)].eq(val) ++ yield self.m_state[1 | (channel << 1) + ++ (1 << w.profile) * w_o.channels].eq(val) + else: + raise ValueError("no such state", coeff) + + def get_state(self, channel, profile=None, coeff="y1"): + """Get a state value.""" + w = self.widths ++ w_o = self.widths_dds + if coeff == "y1": + val = yield self.m_state[profile | (channel << w.profile)] + elif coeff == "x0": +- val = yield self.m_state[(channel << 1) | +- (1 << w.profile + w.channel)] ++ val = yield self.m_state[(channel << 1) + ++ (1 << w.profile) * w_o.channels] + elif coeff == "x1": +- val = yield self.m_state[1 | (channel << 1) | +- (1 << w.profile + w.channel)] ++ val = yield self.m_state[1 | (channel << 1) + ++ (1 << w.profile) * w_o.channels] + else: + raise ValueError("no such state", coeff) + return signed(val, w.state) + ++ def get_accum_ftw(self, channel): ++ val = yield self.m_accum_ftw[channel] ++ return val ++ ++ def get_t_ref(self, channel): ++ val = yield self.m_t_ref[channel] ++ return val ++ + def fast_iter(self): + """Perform a single processing iteration.""" + assert (yield self.done) +@@ -607,6 +731,8 @@ def check_iter(self): + """Perform a single processing iteration while verifying + the behavior.""" + w = self.widths ++ w_i = self.widths_adc ++ w_o = self.widths_dds + + while not (yield self.done): + yield +@@ -622,25 +748,33 @@ def check_iter(self): + + x0s = [] + # check adc loading +- for i in range(1 << w.channel): ++ for i in range(w_i.channels): + v_adc = signed((yield self.adc[i]), w.adc) + x0 = yield from self.get_state(i, coeff="x0") + x0s.append(x0) +- assert v_adc << (w.state - w.adc - 1) == x0, (hex(v_adc), hex(x0)) + logger.debug("adc[%d] adc=%x x0=%x", i, v_adc, x0) ++ assert v_adc << (w.state - w.adc - 1) == x0, (hex(v_adc), hex(x0)) + + data = [] + # predict output +- for i in range(1 << w.channel): ++ for i in range(w_o.channels): ++ t0 = yield self._dt_start ++ dds_ftw_accu = yield from self.get_accum_ftw(i) ++ sysclks_ref = (yield from self.get_t_ref(i)) * self.widths_dds.sysclk_per_clk\ ++ + (yield self.ctrl_reftime[i].sysclks_fine) ++ logger.debug("dt_start=%d dt_ref=%d t_cycle=%d ftw_accu=%#x", ++ t0, sysclks_ref, self.t_cycle, dds_ftw_accu) ++ + j = yield self.ctrl[i].profile + en_iir = yield self.ctrl[i].en_iir + en_out = yield self.ctrl[i].en_out ++ en_pt = yield self.ctrl[i].en_pt + dly_i = yield self._dlys[i] +- logger.debug("ctrl[%d] profile=%d en_iir=%d en_out=%d dly=%d", +- i, j, en_iir, en_out, dly_i) ++ logger.debug("ctrl[%d] profile=%d en_iir=%d en_out=%d en_pt=%d dly=%d", ++ i, j, en_iir, en_out, en_pt, dly_i) + + cfg = yield from self.get_coeff(i, j, "cfg") +- k_j = cfg & ((1 << w.channel) - 1) ++ k_j = cfg & ((1 << bits_for(w_i.channels - 1)) - 1) + dly_j = (cfg >> 8) & 0xff + logger.debug("cfg[%d,%d] sel=%d dly=%d", i, j, k_j, dly_j) + +@@ -657,9 +791,13 @@ def check_iter(self): + + ftw0 = yield from self.get_coeff(i, j, "ftw0") + ftw1 = yield from self.get_coeff(i, j, "ftw1") +- pow = yield from self.get_coeff(i, j, "pow") +- logger.debug("dds[%d,%d] ftw0=%#x ftw1=%#x pow=%#x", +- i, j, ftw0, ftw1, pow) ++ _pow = yield from self.get_coeff(i, j, "pow") ++ ph_coh = ((ftw0 | (ftw1 << w.word)) * (t0 - sysclks_ref)) ++ ph_accu = dds_ftw_accu * self.t_cycle * self.widths_dds.sysclk_per_clk ++ ph = ph_coh - ph_accu ++ pow = (_pow + (ph >> w.word)) & 0xffff if en_pt else _pow ++ logger.debug("dds[%d,%d] ftw0=%#x ftw1=%#x ph_coh=%#x _pow=%#x pow=%#x", ++ i, j, ftw0, ftw1, ph_coh, _pow, pow) + + y1 = yield from self.get_state(i, j, "y1") + x1 = yield from self.get_state(k_j, coeff="x1") +@@ -681,6 +819,10 @@ def check_iter(self): + # wait for output + assert (yield self.processing) + while (yield self.processing): ++ logger.debug("sysclks_to_ref=%d sysclks_ref_to_iter_start=%d", ++ (yield self._sysclks_to_ref), ++ (yield self._sysclks_ref_to_iter_start)) ++ # logger.debug("%d %d %d %d", *[x for x in (yield self._sysclks_ref_fine)]) + yield + + assert (yield self.shifting) +@@ -694,7 +836,7 @@ def check_iter(self): + logger.debug("adc[%d] x0=%x x1=%x", i, x0, x1) + + # check new state +- for i in range(1 << w.channel): ++ for i in range(w_o.channels): + j = yield self.ctrl[i].profile + logger.debug("ch[%d] profile=%d", i, j) + y1 = yield from self.get_state(i, j, "y1") +@@ -702,7 +844,7 @@ def check_iter(self): + assert y1 == y0, (hex(y1), hex(y0)) + + # check dds output +- for i in range(1 << w.channel): ++ for i in range(w_o.channels): + ftw0, ftw1, pow, y0, x1, x0 = data[i] + asf = y0 >> (w.state - w.asf - 1) + dds = (ftw0 | (ftw1 << w.word) | +diff --git a/artiq/gateware/suservo/pads.py b/artiq/gateware/suservo/pads.py +index 0ab7d352f1..bdae8ee35c 100644 +--- a/artiq/gateware/suservo/pads.py ++++ b/artiq/gateware/suservo/pads.py +@@ -1,5 +1,7 @@ + from migen import * + from migen.genlib.io import DifferentialOutput, DifferentialInput, DDROutput ++from artiq.gateware.rtio.phy import ttl_serdes_7series, ttl_serdes_generic ++from artiq.gateware.rtio import rtlink + + + class SamplerPads(Module): +@@ -57,27 +59,85 @@ def __init__(self, platform, eem): + clk=dp.clkout, port=sdop) + + ++class OutIoUpdate_8X(Module): ++ def __init__(self, pad): ++ serdes = ttl_serdes_7series._OSERDESE2_8X() ++ self.submodules += serdes ++ ++ self.passthrough = Signal() ++ self.data = Signal() ++ self.fine_ts = Signal(3) ++ ++ self.rtlink = rtlink.Interface( ++ rtlink.OInterface(1, fine_ts_width=3)) ++ self.probes = [serdes.o[-1]] ++ override_en = Signal() ++ override_o = Signal() ++ self.overrides = [override_en, override_o] ++ ++ # # # ++ ++ self.specials += Instance("IOBUFDS", ++ i_I=serdes.ser_out, ++ i_T=serdes.t_out, ++ io_IO=pad.p, ++ io_IOB=pad.n) ++ ++ # Just strobe always in non-passthrough mode, as self.data is supposed ++ # to be always valid. ++ self.submodules += ttl_serdes_generic._SerdesDriver( ++ serdes.o, ++ Mux(self.passthrough, self.rtlink.o.stb, 1), ++ Mux(self.passthrough, self.rtlink.o.data, self.data), ++ Mux(self.passthrough, self.rtlink.o.fine_ts, self.fine_ts), ++ override_en, override_o) ++ ++ self.comb += self.rtlink.o.busy.eq(~self.passthrough) ++ ++ + class UrukulPads(Module): + def __init__(self, platform, *eems): + spip, spin = [[ + platform.request("{}_qspi_{}".format(eem, pol), 0) + for eem in eems] for pol in "pn"] +- ioup = [platform.request("{}_io_update".format(eem), 0) +- for eem in eems] ++ + self.cs_n = Signal() + self.clk = Signal() + self.io_update = Signal() ++ self.passthrough = Signal() ++ self.dds_reset_sync_in = Signal(reset=0) # sync_in phy (one for all) ++ ++ # # # ++ ++ self.io_update_phys = [] ++ for eem in eems: ++ phy = OutIoUpdate_8X(platform.request("{}_io_update".format(eem), 0)) ++ self.io_update_phys.append(phy) ++ setattr(self.submodules, "{}_io_update_phy".format(eem), phy) ++ self.comb += [ ++ phy.data.eq(self.io_update), ++ phy.passthrough.eq(self.passthrough), ++ ] ++ ++ sync_in_pads = platform.request("{}_dds_reset_sync_in".format(eem)) ++ sync_in_r = Signal() ++ self.sync.rio_phy += sync_in_r.eq(self.dds_reset_sync_in) ++ sync_in_o = Signal() ++ self.specials += Instance("ODDR", ++ p_DDR_CLK_EDGE="SAME_EDGE", ++ i_C=ClockSignal("rio_phy"), i_CE=1, i_S=0, i_R=0, ++ i_D1=sync_in_r, i_D2=sync_in_r, o_Q=sync_in_o) ++ self.specials += DifferentialOutput(sync_in_o, sync_in_pads.p, sync_in_pads.n) ++ + self.specials += [( + DifferentialOutput(~self.cs_n, spip[i].cs, spin[i].cs), +- DifferentialOutput(self.clk, spip[i].clk, spin[i].clk), +- DifferentialOutput(self.io_update, ioup[i].p, ioup[i].n)) ++ DifferentialOutput(self.clk, spip[i].clk, spin[i].clk)) + for i in range(len(eems))] +- for i in range(8): ++ for i in range(4 * len(eems)): + mosi = Signal() + setattr(self, "mosi{}".format(i), mosi) +- for i in range(4*len(eems)): + self.specials += [ +- DifferentialOutput(getattr(self, "mosi{}".format(i)), ++ DifferentialOutput(mosi, + getattr(spip[i // 4], "mosi{}".format(i % 4)), + getattr(spin[i // 4], "mosi{}".format(i % 4))) + ] +diff --git a/artiq/gateware/suservo/servo.py b/artiq/gateware/suservo/servo.py +index 1aec95f027..15d31027e0 100644 +--- a/artiq/gateware/suservo/servo.py ++++ b/artiq/gateware/suservo/servo.py +@@ -42,7 +42,7 @@ def __init__(self, adc_pads, dds_pads, adc_p, iir_p, dds_p): + assert t_iir + 2*adc_p.channels < t_cycle, "need shifting time" + + self.submodules.adc = ADC(adc_pads, adc_p) +- self.submodules.iir = IIR(iir_p) ++ self.submodules.iir = IIR(iir_p, adc_p, dds_p, t_cycle) + self.submodules.dds = DDS(dds_pads, dds_p) + + # adc channels are reversed on Sampler +@@ -63,7 +63,6 @@ def __init__(self, adc_pads, dds_pads, adc_p, iir_p, dds_p): + assert t_restart > 1 + cnt = Signal(max=t_restart) + cnt_done = Signal() +- active = Signal(3) + + # Indicates whether different steps (0: ADC, 1: IIR, 2: DDS) are + # currently active (exposed for simulation only), with each bit being +@@ -71,6 +70,8 @@ def __init__(self, adc_pads, dds_pads, adc_p, iir_p, dds_p): + # timing details of the different steps, any number can be concurrently + # active (e.g. ADC read from iteration n, IIR computation from iteration + # n - 1, and DDS write from iteration n - 2). ++ active = Signal(3) ++ self._active = active # Exposed for debugging only. + + # Asserted once per cycle when the DDS write has been completed. + self.done = Signal() +@@ -95,6 +96,17 @@ def __init__(self, adc_pads, dds_pads, adc_p, iir_p, dds_p): + cnt.eq(t_restart - 1) + ) + ] ++ ++ # Count number of cycles since the servo was last started from idle. ++ self.sync += If(active == 0, ++ self.iir.t_running.eq(0), ++ self.iir.reset_dds_phase.eq(1) ++ ).Else( ++ self.iir.t_running.eq(self.iir.t_running + 1) ++ ) ++ ++ self.sync += dds_pads.passthrough.eq(active == 0) ++ + self.comb += [ + cnt_done.eq(cnt == 0), + self.adc.start.eq(self.start & cnt_done), +diff --git a/artiq/gateware/test/suservo/__init__.py b/artiq/gateware/test/suservo/__init__.py +index e69de29bb2..7a1df77ac1 100644 +--- a/artiq/gateware/test/suservo/__init__.py ++++ b/artiq/gateware/test/suservo/__init__.py +@@ -0,0 +1,10 @@ ++"""Gateware implementation of the Sampler-Urukul (AD9910) DDS amplitude servo. ++ ++General conventions: ++ ++ - ``t_...`` signals and constants refer to time spans measured in the gateware ++ module's default clock (typically a 125 MHz RTIO clock). ++ - ``start`` signals cause modules to proceed with the next servo iteration iff ++ they are currently idle (i.e. their value is irrelevant while the module is ++ busy, so they are not necessarily one-clock-period strobes). ++""" +diff --git a/artiq/gateware/test/suservo/test_dds.py b/artiq/gateware/test/suservo/test_dds.py +index a666f14c56..d9a8167590 100644 +--- a/artiq/gateware/test/suservo/test_dds.py ++++ b/artiq/gateware/test/suservo/test_dds.py +@@ -5,6 +5,9 @@ + + from artiq.gateware.suservo.dds_ser import DDSParams, DDS + ++class OutIoUpdateTB(Module): ++ def __init__(self): ++ self.fine_ts = Signal(3) + + class TB(Module): + def __init__(self, p): +@@ -15,6 +18,12 @@ def __init__(self, p): + setattr(self, "mosi{}".format(i), m) + self.miso = Signal() + self.io_update = Signal() ++ self.passthrough = Signal() ++ ++ self.io_update_phys = [] ++ for i in range(p.channels//4): ++ phy = OutIoUpdateTB() ++ self.io_update_phys.append(phy) + + clk0 = Signal() + self.sync += clk0.eq(self.clk) +@@ -23,16 +32,19 @@ def __init__(self, p): + + self.ddss = [] + for i in range(p.channels): +- dds = Record([("ftw", 32), ("pow", 16), ("asf", 16), ("cmd", 8)]) +- sr = Signal(len(dds)) ++ dds = Record([("ftw", 32), ("pow", 16), ("asf", 16), ++ ("cmd", 8), ("accu", 32), ("phase", 19)]) ++ sr = Signal(32 + 16 + 16 + 8) + self.sync += [ ++ dds.accu.eq(dds.accu + p.sysclk_per_clk * dds.ftw), + If(~self.cs_n & sample, + sr.eq(Cat(self.mosi[i], sr)) + ), + If(self.io_update, +- dds.raw_bits().eq(sr) ++ dds.raw_bits()[:len(sr)].eq(sr) + ) + ] ++ self.comb += dds.phase.eq((dds.pow << 3) + (dds.accu >> 13)) + self.ddss.append(dds) + + @passive +@@ -55,7 +67,7 @@ def log(self, data): + + + def main(): +- p = DDSParams(channels=4, width=8 + 32 + 16 + 16, clk=1) ++ p = DDSParams(channels=4, width=8 + 32 + 16 + 16, clk=1, sysclk_per_clk=8) + tb = TB(p) + dds = DDS(tb, p) + tb.submodules += dds +diff --git a/artiq/gateware/test/suservo/test_iir.py b/artiq/gateware/test/suservo/test_iir.py +index 919e7a6bf9..ab8a9a4a46 100644 +--- a/artiq/gateware/test/suservo/test_iir.py ++++ b/artiq/gateware/test/suservo/test_iir.py +@@ -2,48 +2,67 @@ + import unittest + + from migen import * +-from artiq.gateware.suservo import iir ++from artiq.gateware.suservo import servo ++from collections import namedtuple + ++logger = logging.getLogger(__name__) ++ ++ADCParamsSim = namedtuple("ADCParams", ["channels"]) ++DDSParamsSim = namedtuple("ADCParams", ["channels", "sysclk_per_clk"]) + + def main(): +- w_kasli = iir.IIRWidths(state=25, coeff=18, adc=16, +- asf=14, word=16, accu=48, shift=11, +- channel=3, profile=5, dly=8) +- w = iir.IIRWidths(state=17, coeff=16, adc=16, +- asf=14, word=16, accu=48, shift=11, +- channel=2, profile=1, dly=8) ++ w_kasli = servo.IIRWidths(state=25, coeff=18, adc=16, asf=14, ++ word=16, accu=48, shift=11, profile=5, dly=8) ++ p_adc = ADCParamsSim(channels=8) ++ p_dds = DDSParamsSim(channels=4, sysclk_per_clk=8) ++ w = servo.IIRWidths(state=17, coeff=16, adc=16, asf=14, ++ word=16, accu=48, shift=11, profile=2, dly=8) + ++ t_iir = p_adc.channels + 4*p_dds.channels + 8 + 1 + def run(dut): ++ yield dut.t_running.eq(0) + for i, ch in enumerate(dut.adc): + yield ch.eq(i) + for i, ch in enumerate(dut.ctrl): + yield ch.en_iir.eq(1) + yield ch.en_out.eq(1) + yield ch.profile.eq(i) +- for i in range(1 << w.channel): ++ yield ch.en_pt.eq(i) ++ for i, ch in enumerate(dut.ctrl_reftime): ++ yield ch.sysclks_fine.eq(i) ++ yield ch.stb.eq(1) ++ yield ++ yield dut.t_running.eq(dut.t_running + 1) ++ yield ch.stb.eq(0) ++ yield ++ yield dut.t_running.eq(dut.t_running + 1) ++ for i in range(p_adc.channels): + yield from dut.set_state(i, i << 8, coeff="x1") + yield from dut.set_state(i, i << 8, coeff="x0") ++ for i in range(p_dds.channels): + for j in range(1 << w.profile): + yield from dut.set_state(i, + (j << 1) | (i << 8), profile=j, coeff="y1") + for k, l in enumerate("pow offset ftw0 ftw1".split()): + yield from dut.set_coeff(i, profile=j, coeff=l, +- value=(i << 12) | (j << 8) | (k << 4)) ++ value=(i << 10) | (j << 8) | (k << 4)) + yield +- for i in range(1 << w.channel): ++ for i in range(p_dds.channels): + for j in range(1 << w.profile): +- for k, l in enumerate("cfg a1 b0 b1".split()): ++ for k, l in enumerate("a1 b0 b1".split()): + yield from dut.set_coeff(i, profile=j, coeff=l, +- value=(i << 12) | (j << 8) | (k << 4)) ++ value=(i << 10) | (j << 8) | (k << 4)) + yield from dut.set_coeff(i, profile=j, coeff="cfg", +- value=(i << 0) | (j << 8)) # sel, dly ++ value=(i % p_adc.channels) | (j << 8)) # sel, dly + yield +- for i in range(10): ++ for i in range(4): ++ logger.debug("check_iter {}".format(i)) + yield from dut.check_iter() ++ yield dut.t_running.eq((yield dut.t_running) + t_iir) + yield + +- dut = iir.IIR(w) +- run_simulation(dut, [run(dut)], vcd_name="iir.vcd") ++ dut = servo.IIR(w, p_adc, p_dds, t_iir) ++ run_simulation(dut, [run(dut)], vcd_name="servo.vcd") + + + class IIRTest(unittest.TestCase): +diff --git a/artiq/gateware/test/suservo/test_servo.py b/artiq/gateware/test/suservo/test_servo.py +index cc1a73a2be..fe1708d033 100644 +--- a/artiq/gateware/test/suservo/test_servo.py ++++ b/artiq/gateware/test/suservo/test_servo.py +@@ -1,5 +1,6 @@ + import logging + import unittest ++import numpy as np + + from migen import * + from migen.genlib import io +@@ -7,15 +8,17 @@ + from artiq.gateware.test.suservo import test_adc, test_dds + from artiq.gateware.suservo import servo + ++logger = logging.getLogger(__name__) ++ + + class ServoSim(servo.Servo): + def __init__(self): + adc_p = servo.ADCParams(width=16, channels=8, lanes=4, + t_cnvh=4, t_conv=57 - 4, t_rtt=4 + 4) + iir_p = servo.IIRWidths(state=25, coeff=18, adc=16, asf=14, word=16, +- accu=48, shift=11, channel=3, profile=5, dly=8) ++ accu=48, shift=11, profile=5, dly=8) + dds_p = servo.DDSParams(width=8 + 32 + 16 + 16, +- channels=adc_p.channels, clk=1) ++ channels=4, clk=1, sysclk_per_clk=8) + + self.submodules.adc_tb = test_adc.TB(adc_p) + self.submodules.dds_tb = test_dds.TB(dds_p) +@@ -23,37 +26,156 @@ def __init__(self): + servo.Servo.__init__(self, self.adc_tb, self.dds_tb, + adc_p, iir_p, dds_p) + ++ self.dds_output = [] ++ ++ def log_flow(self, cycle): ++ su_start = yield self.start ++ adc_start = yield self.adc.start ++ iir_start = yield self.iir.start ++ dds_start = yield self.dds.start ++ su_done = yield self.done ++ adc_done = yield self.adc.done ++ iir_done = yield self.iir.done ++ dds_done = yield self.dds.done ++ active = yield self._active ++ io_update = yield self.dds_tb.io_update ++ passthrough = yield self.dds_tb.passthrough ++ iir_loading = yield self.iir.loading ++ iir_processing = yield self.iir.processing ++ iir_shifting = yield self.iir.shifting ++ dt = yield self.iir.t_running ++ dt_iir = yield self.iir._dt_start ++ state = yield self.iir._state ++ stage0 = yield self.iir._stages[0] ++ stage1 = yield self.iir._stages[1] ++ stage2 = yield self.iir._stages[2] ++ logger.debug( ++ "cycle=%d " ++ #"start=[su=%d adc=%d iir=%d dds=%d] " ++ #"done=[su=%d adc=%d iir=%d dds=%d] " ++ "active=%s load_proc_shft=%d%d%d stages_active=%d%d%d " ++ "io_update=%d passthrough=%d " ++ "dt=%d dt_iir=%d state=%d", ++ cycle, ++ #su_start, adc_start, iir_start, dds_start, ++ #su_done, adc_done, iir_done, dds_done, ++ '{:03b}'.format(active), iir_loading, iir_processing, iir_shifting, stage0, stage1, stage2, ++ io_update, passthrough, ++ dt, dt_iir//8, state ++ ) ++ ++ def log_state(self, channel, profile, calls=[0]): ++ calls[0] += 1 ++ # if not (yield self._active[1]): ++ # return ++ yield from self.log_flow(calls[0] - 2) ++ return ++ cfg = yield from self.iir.get_coeff(channel, profile, "cfg") ++ sel = cfg & 0x7 ++ x0 = yield from self.iir.get_state(sel, coeff="x0") ++ x1 = yield from self.iir.get_state(sel, coeff="x1") ++ y1 = yield from self.iir.get_state(channel, profile, coeff="y1") ++ _pow = yield from self.iir.get_coeff(channel, profile, "pow") ++ pow_iir = yield self.iir.dds[channel][2*self.iir.widths.word:3*self.iir.widths.word] ++ pow_dds = yield self.dds_tb.ddss[channel].pow ++ asf_dds = yield self.dds_tb.ddss[channel].asf ++ ftw_dds = yield self.dds_tb.ddss[channel].ftw ++ accu_dds = yield self.dds_tb.ddss[channel].accu ++ phase_dds = (yield self.dds_tb.ddss[channel].phase) ++ dds_output = np.cos(2*np.pi*phase_dds/2**19) ++ ph_coh = yield self.iir._ph_coh ++ ph_acc = yield self.iir._ph_acc ++ offset = yield from self.iir.get_coeff(channel, profile, "offset") ++ ftw0 = yield from self.iir.get_coeff(channel, profile, "ftw0") ++ ftw1 = yield from self.iir.get_coeff(channel, profile, "ftw1") ++ m_phase = yield from self.iir.get_accum_ftw(channel) ++ iir_adc = yield self.iir.adc[sel] ++ logger.debug("\t" ++ "ch=%d pr=%d " ++ # "x0=%d x1=%d adc=%d y1=%d sel=%d " ++ "ftw=%#x pow_coeff=%#x ftw_accu=%#x " ++ "ph_coh=%#x ph_acc=%#x " ++ "pow_iir=%#x pow_dds=%#x ftw_dds=%#x asf_dds=%#x accu_dds=%#x phase_dds=%#x dds_output=%04.3f", ++ channel, profile, ++ # x0, x1, iir_adc, y1, sel, ++ ftw0 | (ftw1 << 16), _pow, m_phase, ++ ph_coh, ph_acc, ++ pow_iir, pow_dds, ftw_dds, asf_dds, accu_dds, phase_dds >> 3, dds_output ++ ) ++ self.dds_output.append(dds_output) ++ # yield from self.log_registers(profile) ++ ++ def log_registers(self, profile): ++ adc_channels = self.iir.widths_adc.channels ++ dds_channels = self.iir.widths_dds.channels ++ x0s = [0]*adc_channels ++ x1s = [0]*adc_channels ++ y1s = [0]*dds_channels ++ for ch in range(adc_channels): ++ x0s[ch] = yield from self.iir.get_state(ch, coeff="x0") ++ x1s[ch] = yield from self.iir.get_state(ch, coeff="x1") ++ for ch in range(dds_channels): ++ y1s[ch] = yield from self.iir.get_state(ch, profile, coeff="y1") ++ ++ logger.debug(("x0s = " + '{:05X} ' * adc_channels).format(*x0s)) ++ logger.debug(("x1s = " + '{:05X} ' * adc_channels).format(*x1s)) ++ logger.debug(("y1s = " + '{:05X} ' * dds_channels).format(*y1s)) ++ + def test(self): + assert (yield self.done) + +- adc = 1 ++ adc = 7 + x0 = 0x0141 + yield self.adc_tb.data[-adc-1].eq(x0) +- channel = 3 +- yield self.iir.adc[channel].eq(adc) ++ channel = 0 + yield self.iir.ctrl[channel].en_iir.eq(1) + yield self.iir.ctrl[channel].en_out.eq(1) +- profile = 5 ++ yield self.iir.ctrl[channel].en_pt.eq(1) ++ profile = 31 + yield self.iir.ctrl[channel].profile.eq(profile) + x1 = 0x0743 + yield from self.iir.set_state(adc, x1, coeff="x1") + y1 = 0x1145 + yield from self.iir.set_state(channel, y1, + profile=profile, coeff="y1") +- coeff = dict(pow=0x1333, offset=0x1531, ftw0=0x1727, ftw1=0x1929, +- a1=0x0135, b0=0x0337, b1=0x0539, cfg=adc | (0 << 3)) ++ coeff = dict(pow=0, offset=0x1531, ftw0=0xeb85, ftw1=0x51, ++ a1=0x0135, b0=0x0337, b1=0x0539, cfg=adc) + for ks in "pow offset ftw0 ftw1", "a1 b0 b1 cfg": + for k in ks.split(): + yield from self.iir.set_coeff(channel, value=coeff[k], + profile=profile, coeff=k) + yield + ++ num_it = 1 ++ num_proc_its = [0]*num_it # number of iterations while iir.processing ++ yield from self.log_state(channel, profile) + yield self.start.eq(1) + yield +- yield self.start.eq(0) +- while not (yield self.dds_tb.io_update): +- yield +- yield # io_update ++ for i in range(num_it): ++ if i == 1: # change ftw ++ yield from self.iir.set_coeff(channel, ++ profile=profile, coeff='ftw0', value=coeff['ftw1']) ++ yield from self.iir.set_coeff(channel, ++ profile=profile, coeff='ftw1', value=coeff['ftw0']) ++ if i == 2: # change ftw back ++ yield from self.iir.set_coeff(channel, ++ profile=profile, coeff='ftw0', value=coeff['ftw0']) ++ yield from self.iir.set_coeff(channel, ++ profile=profile, coeff='ftw1', value=coeff['ftw1']) ++ logger.debug("iteration {}".format(i)) ++ yield from self.log_state(channel, profile) ++ if i == num_it-1: ++ yield self.start.eq(0) ++ while not (yield self.dds_tb.io_update): ++ yield ++ if (yield self.iir.processing): ++ num_proc_its[i] += 1 ++ if (yield self.iir._stages) != 0: ++ yield from self.log_state(channel, profile) ++ yield # io_update ++ yield from self.log_state(channel, profile) ++ yield ++ yield from self.log_state(channel, profile) + + w = self.iir.widths + +@@ -63,6 +185,8 @@ def test(self): + + offset = coeff["offset"] << (w.state - w.coeff - 1) + a1, b0, b1 = coeff["a1"], coeff["b0"], coeff["b1"] ++ ++ # works only for 1 iteration + out = ( + 0*(1 << w.shift - 1) + # rounding + a1*(y1 + 0) + b0*(x0 + offset) + b1*(x1 + offset) +@@ -76,8 +200,15 @@ def test(self): + ftw = (coeff["ftw1"] << 16) | coeff["ftw0"] + assert _ == ftw, (hex(_), hex(ftw)) + ++ t0 = yield self.iir._dt_start ++ # todo: include phase accumulator ++ ph = (ftw * t0) >> 16 ++ if (yield self.iir.ctrl[channel].en_pt): ++ pow = (coeff["pow"] + ph) & 0xffff ++ else: ++ pow = coeff["pow"] + _ = yield self.dds_tb.ddss[channel].pow +- assert _ == coeff["pow"], (hex(_), hex(coeff["pow"])) ++ assert _ == pow, (hex(_), hex(pow)) + + _ = yield self.dds_tb.ddss[channel].asf + asf = y1 >> (w.state - w.asf - 1) +@@ -101,4 +232,5 @@ def test_run(self): + + + if __name__ == "__main__": ++ logging.basicConfig(level=logging.DEBUG) + main() From a028b5c9f7c675e9dab8ef30fe4de90bb1069626 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Thu, 15 Sep 2022 09:15:38 +0800 Subject: [PATCH 31/72] afws_client: update --- artiq/frontend/afws_client.py | 36 ++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/artiq/frontend/afws_client.py b/artiq/frontend/afws_client.py index 25ddfc1f9..d5322928e 100755 --- a/artiq/frontend/afws_client.py +++ b/artiq/frontend/afws_client.py @@ -29,7 +29,19 @@ def get_artiq_rev(): import artiq except ImportError: return None - return artiq._version.get_rev() + rev = artiq._version.get_rev() + if rev == "unknown": + return None + return rev + + +def get_artiq_major_version(): + try: + import artiq + except ImportError: + return None + version = artiq._version.get_version() + return version.split(".")[0] def zip_unarchive(data, directory): @@ -70,17 +82,20 @@ class Client: self.send_command("LOGIN", username, password) return self.read_reply() == ["HELLO"] - def build(self, rev, variant, log): + def build(self, major_ver, rev, variant, log): if not variant: variants = self.get_variants() if len(variants) != 1: raise ValueError("User can build more than 1 variant - need to specify") variant = variants[0][0] print("Building variant: {}".format(variant)) - if log: - self.send_command("BUILD", rev, variant, "LOG_ENABLE") - else: - self.send_command("BUILD", rev, variant) + build_args = ( + rev, + variant, + "LOG_ENABLE" if log else "LOG_DISABLE", + major_ver, + ) + self.send_command("BUILD", *build_args) reply = self.read_reply()[0] if reply != "BUILDING": return reply, None @@ -134,6 +149,7 @@ def main(): action = parser.add_subparsers(dest="action") action.required = True act_build = action.add_parser("build", help="build and download firmware") + act_build.add_argument("--major-ver", default=None, help="ARTIQ major version") act_build.add_argument("--rev", default=None, help="revision to build (default: currently installed ARTIQ revision)") act_build.add_argument("--log", action="store_true", help="Display the build log") act_build.add_argument("directory", help="output directory") @@ -182,13 +198,19 @@ def main(): except NotADirectoryError: print("A file with the same name as the output directory already exists. Please remove it and try again.") sys.exit(1) + major_ver = args.major_ver + if major_ver is None: + major_ver = get_artiq_major_version() + if major_ver is None: + print("Unable to determine currently installed ARTIQ major version. Specify manually using --major-ver.") + sys.exit(1) rev = args.rev if rev is None: rev = get_artiq_rev() if rev is None: print("Unable to determine currently installed ARTIQ revision. Specify manually using --rev.") sys.exit(1) - result, contents = client.build(rev, args.variant, args.log) + result, contents = client.build(major_ver, rev, args.variant, args.log) if result != "OK": if result == "UNAUTHORIZED": print("You are not authorized to build this variant. Your firmware subscription may have expired. Contact helpdesk\x40m-labs.hk.") From 14ab1d4bbcfe7aaf99b6121f4e0a116bf8dd1768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 15 Sep 2022 11:02:59 +0000 Subject: [PATCH 32/72] miqro format change: encode len, not end --- artiq/coredevice/phaser.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index fc61468d9..96f9172b0 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -1386,8 +1386,8 @@ class Miqro: for osc in range(16): for pro in range(32): self.set_profile_mu(osc, pro, 0, 0, 0) - delay(20*us) - self.set_window_mu(start=0, iq=[0], rate=1, shift=0, order=0, head=0, tail=1) + delay(10*us) + self.set_window_mu(start=0, iq=[0], rate=1, shift=0, order=0, head=0, tail=0) self.pulse(window=0, profiles=[0]) @kernel @@ -1412,6 +1412,7 @@ class Miqro: raise ValueError("amplitude out of bounds") pow = int32(round(phase*(1 << 16))) self.set_profile_mu(oscillator, profile, ftw, asf, pow) + return ftw @kernel def set_window_mu(self, start, iq, rate=1, shift=0, order=3, head=1, tail=1): @@ -1428,7 +1429,7 @@ class Miqro: self.channel.phaser.write16(PHASER_ADDR_MIQRO_MEM_ADDR, (self.channel.index << 15) | start) self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, - ((start + 1 + len(iq)) & 0x3ff) | + (len(iq) & 0x3ff) | ((rate - 1) << 10) | (shift << 22) | (order << 28) | @@ -1457,6 +1458,7 @@ class Miqro: for iqi in iq ] self.set_window_mu(start, iq_mu, rate, shift, order, head, tail) + return rate @kernel def encode(self, window, profiles, data): @@ -1470,20 +1472,20 @@ class Miqro: for profile in profiles: if profile > 0x1f: raise ValueError("profile out of bounds") - if idx >= 30: - word += 1 - idx = 0 data[word] |= profile << idx idx += 5 - return word + 1 + if idx > 32 - 5: + word += 1 + idx = 0 + return word @kernel def pulse_mu(self, data): word = len(data) delay_mu(-8*word) # back shift to align while word > 0: - delay_mu(8) word -= 1 + delay_mu(8) # final write sets pulse stb rtio_output(self.base_addr + word, data[word]) From aedcf205c776f17bc87f6b9e455ba4761c0fc0e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 16 Sep 2022 12:14:59 +0000 Subject: [PATCH 33/72] miqro: docs --- artiq/coredevice/phaser.py | 174 ++++++++++++++++++++++--------------- 1 file changed, 104 insertions(+), 70 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 96f9172b0..9f628611f 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -47,7 +47,7 @@ PHASER_ADDR_SERVO_CFG1 = 0x31 # 0x32 - 0x71 servo coefficients + offset data PHASER_ADDR_SERVO_DATA_BASE = 0x32 -# 0x78 Miqro channel profile/window memories +# 0x72 - 0x78 Miqro channel profile/window memories PHASER_ADDR_MIQRO_MEM_ADDR = 0x72 PHASER_ADDR_MIQRO_MEM_DATA = 0x74 @@ -84,6 +84,19 @@ class Phaser: Phaser contains a 4 channel, 1 GS/s DAC chip with integrated upconversion, quadrature modulation compensation and interpolation features. + The coredevice RTIO PHY and the Phaser gateware come in different modes + that have different features. Phaser mode and coredevice PHY are both + both selected at gateware compile-time and need to match. + + Phaser gateware | Coredevice PHY | Features per :class:`PhaserChannel` + --------------- | -------------- | ----------------------------------- + Base <= v0.5 | Base | Base (5 :class:`PhaserOscillator`) + Base >= v0.6 | Base | Base + Servo + Miqro >= v0.6 | Miqro | :class:`Miqro` + + Base mode + --------- + The coredevice produces 2 IQ (in-phase and quadrature) data streams with 25 MS/s and 14 bit per quadrature. Each data stream supports 5 independent numerically controlled IQ oscillators (NCOs, DDSs with 32 bit frequency, 16 @@ -114,6 +127,16 @@ class Phaser: absolute phase with respect to other RTIO input and output events (see `get_next_frame_mu()`). + Miqro mode + ---------- + + See :class:`Miqro` + + Here the DAC operates in 4x interpolation. + + Analog flow + ----------- + The four analog DAC outputs are passed through anti-aliasing filters. In the baseband variant, the even/in-phase DAC channels feed 31.5 dB range @@ -131,6 +154,9 @@ class Phaser: configured through a shared SPI bus that is accessed and controlled via FPGA registers. + Servo + ----- + 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 @@ -1290,90 +1316,98 @@ class Miqro: """ Miqro pulse generator. - Notes - ----- - * The annotation that some operation is "expensive" does not mean it is impossible, just - that it may take a significant amount of time and resources to execute such that - it may be impractical when used often or during fast pulse sequences. - They are intended for use in calibration and initialization. + A Miqro instance represents one RF output. The DSP components are fully + contained in the Phaser gateware. The output is generated by with + the following data flow: - Functionality - ------------- - A Miqro instance represents one RF output. - The output is generated by with the following data flow: + Oscillators + ........... - ### Oscillators * There are n_osc = 16 oscillators with oscillator IDs 0..n_osc-1. * Each oscillator outputs one tone at any given time - I/Q (quadrature, a.k.a. complex) 2x16 bit signed data - at tau = 4 ns sample intervals, 250 MS/s, Nyquist 125 MHz, bandwidth 200 MHz - (from f = -100..+100 MHz, taking into account the interpolation anti-aliasing - filters in subsequent interpolators), - 32 bit frequency (f) resolution (~ 1/16 Hz), - 16 bit unsigned amplitude (a) resolution - 16 bit phase (p) resolution + + * I/Q (quadrature, a.k.a. complex) 2x16 bit signed data + at tau = 4 ns sample intervals, 250 MS/s, Nyquist 125 MHz, bandwidth 200 MHz + (from f = -100..+100 MHz, taking into account the interpolation anti-aliasing + filters in subsequent interpolators), + * 32 bit frequency (f) resolution (~ 1/16 Hz), + * 16 bit unsigned amplitude (a) resolution + * 16 bit phase offset (p) resolution + * The output phase p' of each oscillator at time t (boot/reset/initialization of the - device at t=0) is then p' = f*t + p (mod 1 turn) where f and p are the - (currently active) profile frequency and phase. - The terms "phase coherent" and "phase tracking" are defined to refer to this - choice of oscillator output phase p'. - Note that the phase p is not accumulated (on top of previous - phases, previous profiles, or oscillator history). It is "absolute" in the - sense that frequency f and phase p fully determine oscillator - output phase p' at time t. This is unlike typical DDS behavior. + device at t=0) is then p' = f*t + p (mod 1 turn) where f and p are the (currently + active) profile frequency and phase offset. + * Note: The terms "phase coherent" and "phase tracking" are defined to refer to this + choice of oscillator output phase p'. Note that the phase offset p is not relative to + (on top of previous phase/profiles/oscillator history). + It is "absolute" in the sense that frequency f and phase offset p fully determine + oscillator output phase p' at time t. This is unlike typical DDS behavior. * Frequency, phase, and amplitude of each oscillator are configurable by selecting one of - n_profile = 32 profiles 0..n_profile-1. This selection is fast and can be done for - each pulse. The phase coherence defined above is guaranteed for each - profile individually. + n_profile = 32 profiles 0..n_profile-1. This selection is fast and can be done for + each pulse. The phase coherence defined above is guaranteed for each + profile individually. * Note: one profile per oscillator (usually profile index 0) should be reserved - for the NOP (no operation, identity) profile, usually with zero - amplitude. + for the NOP (no operation, identity) profile, usually with zero amplitude. * Data for each profile for each oscillator can be configured - individually. Storing profile data should be considered "expensive". + individually. Storing profile data should be considered "expensive". + * Note: The annotation that some operation is "expensive" does not mean it is + impossible, just that it may take a significant amount of time and + resources to execute such that it may be impractical when used often or + during fast pulse sequences. They are intended for use in calibration and + initialization. + + Summation + ......... - ### Summation * The oscillator outputs are added together (wrapping addition). - * The user must ensure that the sum of oscillators outputs does - not exceed the (16 bit signed) data range. In general that means that the sum of the - amplitudes must not exceed the range. + * The user must ensure that the sum of oscillators outputs does not exceed the + data range. In general that means that the sum of the amplitudes must not + exceed one. - ### Shaper - * The summed output stream is then multiplied with a the complex-valued output of a - triggerable shaper. - * Triggering the shaper corresponds to passing a pulse from all - oscillators to the RF output. - * Selected profiles become active simultaneously - (on the same output sample) when triggering the shaper. - * The shaper reads (replays) window samples from a memory of size n_window = 1 << 10 starting - and stopping at memory locations specified. - * Each window memory segment starts with a header determining segment - length and interpolation parameters. - * The window samples are interpolated by a factor (rate change) - between 1 and r = 1 << 12. - * The interpolation order is constant, linear, quadratic, or cubic. This - corresponds to interpolation modes from rectangular window (1st order CIC) - or zero order hold) and to Parzen window (4th order CIC, cubic spline). - * This results in support for pulse lengths of between tau and a bit more than - r * n_window * tau = (1 << 12 + 10) tau ~ 17 ms. - * Windows can be configured to be head-less and/or tail-less, meaning, they - do not feed zero-amplitude samples into the shaper before and after - each window. This is used to implement pulses with arbitrary length or - CW output. + Shaper + ...... + + * The summed complex output stream is then multiplied with a the complex-valued + output of a triggerable shaper. + * Triggering the shaper corresponds to passing a pulse from all oscillators to + the RF output. + * Selected profiles become active simultaneously (on the same output sample) when + triggering the shaper with the first shaper output sample. + * The shaper reads (replays) window samples from a memory of size n_window = 1 << 10. * The window memory can be segmented by choosing different start indices - to support different windows. + to support different windows. + * Each window memory segment starts with a header determining segment + length and interpolation parameters. + * The window samples are interpolated by a factor (rate change) between 1 and + r = 1 << 12. + * The interpolation order is constant, linear, quadratic, or cubic. This + corresponds to interpolation modes from rectangular window (1st order CIC) + or zero order hold) to Parzen window (4th order CIC or cubic spline). + * This results in support for single shot pulse lengths (envelope support) between + tau and a bit more than r * n_window * tau = (1 << 12 + 10) tau ~ 17 ms. + * Windows can be configured to be head-less and/or tail-less, meaning, they + do not feed zero-amplitude samples into the shaper before and after + each window respectively. This is used to implement pulses with arbitrary + length or CW output. + + Overall properties + .................. - ### Overall properties * The DAC may upconvert the signal by applying a frequency offset f1 with - phase p1. + phase p1. * In the Upconverter Phaser variant, the analog quadrature upconverter - applies another frequency of f2 and phase p2. - * The resulting phase of the signal at the SMA output is - (f + f1 + f2)*t + p + s(t - t0) + p1 + p2 (mod 1 turn) - where s(t - t0) is the phase of the interpolated - shaper output, and t0 is the trigger time (fiducial of the shaper). - Unsurprisingly the frequency is the derivative of the phase. + applies another frequency of f2 and phase p2. + * The resulting phase of the signal from one oscillator at the SMA output is + (f + f1 + f2)*t + p + s(t - t0) + p1 + p2 (mod 1 turn) + where s(t - t0) is the phase of the interpolated + shaper output, and t0 is the trigger time (fiducial of the shaper). + Unsurprisingly the frequency is the derivative of the phase. + * Group delays between pulse parameter updates are matched across oscillators, + shapers, and channels. * The minimum time to change profiles and phase offsets is ~128 ns (estimate, TBC). - This is the minimum pulse interval. + This is the minimum pulse interval. + The sustained pulse rate of the RTIO PHY/Fastlink is one pulse per Fastlink frame + (may be increased, TBC). """ def __init__(self, channel): @@ -1458,7 +1492,7 @@ class Miqro: for iqi in iq ] self.set_window_mu(start, iq_mu, rate, shift, order, head, tail) - return rate + return rate*4*ns @kernel def encode(self, window, profiles, data): From 616ed3dcc2876f294ae0670e845dba42e56f4c32 Mon Sep 17 00:00:00 2001 From: mwojcik Date: Fri, 19 Aug 2022 17:23:17 +0800 Subject: [PATCH 34/72] moninj: dds inj: extract shared code detect urukul already init in more than one way detect ad9912 channel already init --- artiq/dashboard/moninj.py | 133 +++++++++++++++++++++++--------------- 1 file changed, 80 insertions(+), 53 deletions(-) diff --git a/artiq/dashboard/moninj.py b/artiq/dashboard/moninj.py index 1a58ad90d..726ce9fb2 100644 --- a/artiq/dashboard/moninj.py +++ b/artiq/dashboard/moninj.py @@ -9,7 +9,7 @@ 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.coredevice.ad9912_reg import AD9912_POW1, AD9912_SER_CONF from artiq.gui.tools import LayoutWidget from artiq.gui.flowlayout import FlowLayout @@ -549,33 +549,56 @@ class _DeviceManager: scheduling["flush"]) logger.info("Submitted '%s', RID is %d", title, rid) - def dds_set_frequency(self, dds_channel, dds_model, freq): + def _dds_faux_injection(self, dds_channel, dds_model, action, title, log_msg): # create kernel and fill it in and send-by-content + + # initialize CPLD (if applicable) if dds_model.is_urukul: + # TODO: reimplement cache (simple "was init") + # + # 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_model.cpld) - cpld_init = """cfg = self.core_cache.get("_{cpld}_cfg") - if len(cfg) > 0: - self.{cpld}.cfg_reg = cfg[0] - else: + + # `sta`/`rf_sw`` variables are guaranteed for urukuls + # so {action} can use it + # if there's no RF enabled, CPLD may have not been initialized + # + # but if there is, it has + cpld_init = """delay(15*ms) + was_init = self.core_cache.get("_{cpld}_init") + sta = self.{cpld}.sta_read() + rf_sw = urukul_sta_rf_sw(sta) + if rf_sw == 0 and len(was_init) == 0: delay(15*ms) self.{cpld}.init() - self.core_cache.put("_{cpld}_cfg", [self.{cpld}.cfg_reg]) - cfg = self.core_cache.get("_{cpld}_cfg") + self.core_cache.put("_{cpld}_init", [1]) """.format(cpld=dds_model.cpld) - cfg_sw = """self.{}.cfg_sw(True) - cfg[0] = self.{}.cfg_reg - """.format(dds_channel, dds_model.cpld) else: cpld_dev = "" cpld_init = "" - cfg_sw = "" + + # AD9912/9910: init channel (if uninitialized) + if dds_model.dds_type == "AD9912": + # 0xFF before init, 0x99 after + channel_init = """ + delay(10*ms) + if self.{dds_channel}.read({cfgreg}, length=1) == 0xFF: + delay(10*ms) + self.{dds_channel}.init() + """.format(dds_channel=dds_channel, cfgreg=AD9912_SER_CONF) + elif dds_model.dds_type == "AD9910": + # TODO: verify AD9910 behavior (when we have hardware) + channel_init = "self.{dds_channel}.init()".format(dds_channel) + else: + channel_init = "self.{dds_channel}.init()".format(dds_channel) + dds_exp = textwrap.dedent(""" from artiq.experiment import * + from artiq.coredevice.urukul import * - class SetDDS(EnvExperiment): + class {title}(EnvExperiment): def build(self): self.setattr_device("core") self.setattr_device("{dds_channel}") @@ -585,53 +608,57 @@ class _DeviceManager: def run(self): self.core.break_realtime() {cpld_init} - delay(5*ms) - self.{dds_channel}.init() - self.{dds_channel}.set({freq}) - {cfg_sw} - """.format(dds_channel=dds_channel, freq=freq, + {channel_init} + delay(15*ms) + {action} + """.format(title=title, action=action, + dds_channel=dds_channel, cpld_dev=cpld_dev, cpld_init=cpld_init, - cfg_sw=cfg_sw)) + channel_init=channel_init)) asyncio.ensure_future( self._submit_by_content( dds_exp, - "SetDDS", - "Set DDS {} {}MHz".format(dds_channel, freq/1e6))) + title, + log_msg)) + + def dds_set_frequency(self, dds_channel, dds_model, freq): + if dds_model.is_urukul: + ch_no = "ch_no = self.{ch}.chip_select - 4" + else: + ch_no = "ch_no = self.{ch}.channel" + action = """ + {ch_no} + self.{ch}.set({freq}) + self.{cpld}.cfg_switches(rf_sw | 1 << ch_no) + """.format(freq=freq, cpld=dds_model.cpld, + ch=dds_channel, ch_no=ch_no.format(ch=dds_channel)) + self._dds_faux_injection( + dds_channel, + dds_model, + action, + "SetDDS", + "Set DDS {} {}MHz".format(dds_channel, freq/1e6)) def dds_channel_toggle(self, dds_channel, dds_model, sw=True): # urukul only - toggle_exp = textwrap.dedent(""" - from artiq.experiment import * - - class ToggleDDS(EnvExperiment): - def build(self): - self.setattr_device("core") - self.setattr_device("{ch}") - self.setattr_device("core_cache") - self.setattr_device("{cpld}") - - @kernel - def run(self): - self.core.break_realtime() - 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_model.cpld, sw=sw)) - asyncio.ensure_future( - self._submit_by_content( - toggle_exp, - "ToggleDDS", - "Toggle DDS {} {}".format(dds_channel, "on" if sw else "off")) + if sw: + switch = "| 1 << ch_no" + else: + switch = "& ~(1 << ch_no)" + action = """ + ch_no = self.{dds_channel}.chip_select - 4 + self.{cpld}.cfg_switches(rf_sw {switch}) + """.format( + dds_channel=dds_channel, + cpld=dds_model.cpld, + switch=switch ) + self._dds_faux_injection( + dds_channel, + dds_model, + action, + "ToggleDDS", + "Toggle DDS {} {}".format(dds_channel, "on" if sw else "off")) def setup_ttl_monitoring(self, enable, channel): if self.mi_connection is not None: From f2c3f9504066a73ef3a9d3062531d027feb58922 Mon Sep 17 00:00:00 2001 From: mwojcik Date: Mon, 22 Aug 2022 10:29:42 +0800 Subject: [PATCH 35/72] moninj: fix ad9914 behavior, comment cleanup --- artiq/dashboard/moninj.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/artiq/dashboard/moninj.py b/artiq/dashboard/moninj.py index 726ce9fb2..ec987d874 100644 --- a/artiq/dashboard/moninj.py +++ b/artiq/dashboard/moninj.py @@ -554,9 +554,6 @@ class _DeviceManager: # initialize CPLD (if applicable) if dds_model.is_urukul: - # TODO: reimplement cache (simple "was init") - # - # urukuls need CPLD init and switch to on cpld_dev = """self.setattr_device("core_cache") self.setattr_device("{}")""".format(dds_model.cpld) @@ -564,8 +561,7 @@ class _DeviceManager: # `sta`/`rf_sw`` variables are guaranteed for urukuls # so {action} can use it # if there's no RF enabled, CPLD may have not been initialized - # - # but if there is, it has + # but if there is, it has been initialised - no need to do again cpld_init = """delay(15*ms) was_init = self.core_cache.get("_{cpld}_init") sta = self.{cpld}.sta_read() @@ -583,16 +579,15 @@ class _DeviceManager: if dds_model.dds_type == "AD9912": # 0xFF before init, 0x99 after channel_init = """ - delay(10*ms) if self.{dds_channel}.read({cfgreg}, length=1) == 0xFF: delay(10*ms) self.{dds_channel}.init() """.format(dds_channel=dds_channel, cfgreg=AD9912_SER_CONF) elif dds_model.dds_type == "AD9910": # TODO: verify AD9910 behavior (when we have hardware) - channel_init = "self.{dds_channel}.init()".format(dds_channel) + channel_init = "self.{dds_channel}.init()".format(dds_channel=dds_channel) else: - channel_init = "self.{dds_channel}.init()".format(dds_channel) + channel_init = "self.{dds_channel}.init()".format(dds_channel=dds_channel) dds_exp = textwrap.dedent(""" from artiq.experiment import * @@ -608,6 +603,7 @@ class _DeviceManager: def run(self): self.core.break_realtime() {cpld_init} + delay(10*ms) {channel_init} delay(15*ms) {action} @@ -622,16 +618,13 @@ class _DeviceManager: log_msg)) def dds_set_frequency(self, dds_channel, dds_model, freq): + action = "self.{ch}.set({freq})".format( + freq=freq, ch=dds_channel) if dds_model.is_urukul: - ch_no = "ch_no = self.{ch}.chip_select - 4" - else: - ch_no = "ch_no = self.{ch}.channel" - action = """ - {ch_no} - self.{ch}.set({freq}) + action += """ + ch_no = self.{ch}.chip_select - 4 self.{cpld}.cfg_switches(rf_sw | 1 << ch_no) - """.format(freq=freq, cpld=dds_model.cpld, - ch=dds_channel, ch_no=ch_no.format(ch=dds_channel)) + """.format(ch=dds_channel, cpld=dds_model.cpld) self._dds_faux_injection( dds_channel, dds_model, From 81ef484864b6727675780df1cf71a68fd3db7230 Mon Sep 17 00:00:00 2001 From: mwojcik Date: Thu, 8 Sep 2022 15:55:20 +0800 Subject: [PATCH 36/72] dashboard moninj: check if ad9910 was init --- artiq/dashboard/moninj.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/artiq/dashboard/moninj.py b/artiq/dashboard/moninj.py index ec987d874..b6c388022 100644 --- a/artiq/dashboard/moninj.py +++ b/artiq/dashboard/moninj.py @@ -8,7 +8,10 @@ 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.ad9910 import ( + _AD9910_REG_PROFILE0, _AD9910_REG_PROFILE7, + _AD9910_REG_FTW, _AD9910_REG_CFR1 +) from artiq.coredevice.ad9912_reg import AD9912_POW1, AD9912_SER_CONF from artiq.gui.tools import LayoutWidget from artiq.gui.flowlayout import FlowLayout @@ -584,8 +587,12 @@ class _DeviceManager: self.{dds_channel}.init() """.format(dds_channel=dds_channel, cfgreg=AD9912_SER_CONF) elif dds_model.dds_type == "AD9910": - # TODO: verify AD9910 behavior (when we have hardware) - channel_init = "self.{dds_channel}.init()".format(dds_channel=dds_channel) + # -1 before init, 2 after + channel_init = """ + if self.{dds_channel}.read32({cfgreg}) == -1: + delay(10*ms) + self.{dds_channel}.init() + """.format(dds_channel=dds_channel, cfgreg=AD9912_SER_CONF) else: channel_init = "self.{dds_channel}.init()".format(dds_channel=dds_channel) From c955ac15ed09c57111678aea3db322c301852642 Mon Sep 17 00:00:00 2001 From: mwojcik Date: Thu, 8 Sep 2022 16:44:56 +0800 Subject: [PATCH 37/72] dashboard moninj: add tooltip for off button --- artiq/dashboard/moninj.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/artiq/dashboard/moninj.py b/artiq/dashboard/moninj.py index b6c388022..3ee97b897 100644 --- a/artiq/dashboard/moninj.py +++ b/artiq/dashboard/moninj.py @@ -314,6 +314,10 @@ class _DDSWidget(QtWidgets.QFrame): apply.clicked.connect(self.apply_changes) if self.dds_model.is_urukul: off_btn.clicked.connect(self.off_clicked) + off_btn.setToolTip(textwrap.dedent( + """Note: If TTL RTIO sw for the channel is switched high, + this button will not disable the channel. + Use the TTL override instead.""")) self.value_edit.returnPressed.connect(lambda: self.apply_changes(None)) self.value_edit.escapePressedConnect(self.cancel_changes) cancel.clicked.connect(self.cancel_changes) From 4266beeb9cb0c9a6a6a697358a6b6a3157f37aee Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 19 Sep 2022 16:57:53 +0800 Subject: [PATCH 38/72] experimental-features: rename patches to be compatible with AFWS server sanitize() --- .../{suservo-coherent.diff => suservo_coherent.diff} | 0 .../{suservo-var-urukul.diff => suservo_var_urukul.diff} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename experimental-features/{suservo-coherent.diff => suservo_coherent.diff} (100%) rename experimental-features/{suservo-var-urukul.diff => suservo_var_urukul.diff} (100%) diff --git a/experimental-features/suservo-coherent.diff b/experimental-features/suservo_coherent.diff similarity index 100% rename from experimental-features/suservo-coherent.diff rename to experimental-features/suservo_coherent.diff diff --git a/experimental-features/suservo-var-urukul.diff b/experimental-features/suservo_var_urukul.diff similarity index 100% rename from experimental-features/suservo-var-urukul.diff rename to experimental-features/suservo_var_urukul.diff From 1709cf9717eb4d7a0ea0c524319d41867d124f9f Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 19 Sep 2022 16:58:41 +0800 Subject: [PATCH 39/72] afws_client: update --- artiq/frontend/afws_client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/artiq/frontend/afws_client.py b/artiq/frontend/afws_client.py index d5322928e..e3a32187a 100755 --- a/artiq/frontend/afws_client.py +++ b/artiq/frontend/afws_client.py @@ -82,7 +82,7 @@ class Client: self.send_command("LOGIN", username, password) return self.read_reply() == ["HELLO"] - def build(self, major_ver, rev, variant, log): + def build(self, major_ver, rev, variant, log, experimental_features): if not variant: variants = self.get_variants() if len(variants) != 1: @@ -94,6 +94,7 @@ class Client: variant, "LOG_ENABLE" if log else "LOG_DISABLE", major_ver, + *experimental_features, ) self.send_command("BUILD", *build_args) reply = self.read_reply()[0] @@ -152,6 +153,7 @@ def main(): act_build.add_argument("--major-ver", default=None, help="ARTIQ major version") act_build.add_argument("--rev", default=None, help="revision to build (default: currently installed ARTIQ revision)") act_build.add_argument("--log", action="store_true", help="Display the build log") + act_build.add_argument("--experimental", action="append", default=[], help="enable an experimental feature (can be repeatedly specified to enable multiple features)") act_build.add_argument("directory", help="output directory") act_build.add_argument("variant", nargs="?", default=None, help="variant to build (can be omitted if user is authorised to build only one)") act_passwd = action.add_parser("passwd", help="change password") @@ -210,7 +212,7 @@ def main(): if rev is None: print("Unable to determine currently installed ARTIQ revision. Specify manually using --rev.") sys.exit(1) - result, contents = client.build(major_ver, rev, args.variant, args.log) + result, contents = client.build(major_ver, rev, args.variant, args.log, args.experimental) if result != "OK": if result == "UNAUTHORIZED": print("You are not authorized to build this variant. Your firmware subscription may have expired. Contact helpdesk\x40m-labs.hk.") From 0e4a87826cbb6fcfe8ef1e6f06f3a665bb2207f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 20 Sep 2022 14:35:06 +0000 Subject: [PATCH 40/72] return pulse support --- artiq/coredevice/phaser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 9f628611f..94012ad69 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -1492,7 +1492,7 @@ class Miqro: for iqi in iq ] self.set_window_mu(start, iq_mu, rate, shift, order, head, tail) - return rate*4*ns + return (len(iq) + order)*rate*4*ns @kernel def encode(self, window, profiles, data): From 5cfa8d9a42121cb050412388aed4468dd35bb888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 23 Sep 2022 11:54:40 +0000 Subject: [PATCH 41/72] add tester support, refactor gateware mode --- artiq/coredevice/phaser.py | 17 +++++++----- artiq/frontend/artiq_ddb_template.py | 14 ++++++---- artiq/frontend/artiq_sinara_tester.py | 39 ++++++++++++++++++--------- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 94012ad69..4afe0c992 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -9,6 +9,10 @@ from artiq.coredevice.trf372017 import TRF372017 PHASER_BOARD_ID = 19 + +PHASER_GW_BASE = 1 +PHASER_GW_MIQRO = 2 + PHASER_ADDR_BOARD_ID = 0x00 PHASER_ADDR_HW_REV = 0x01 PHASER_ADDR_GW_REV = 0x02 @@ -222,7 +226,7 @@ class Phaser: def __init__(self, dmgr, channel_base, miso_delay=1, tune_fifo_offset=True, clk_sel=0, sync_dly=0, dac=None, trf0=None, trf1=None, - mode="base", core_device="core"): + core_device="core"): self.channel_base = channel_base self.core = dmgr.get(core_device) # TODO: auto-align miso-delay in phy @@ -235,6 +239,7 @@ class Phaser: self.clk_sel = clk_sel self.tune_fifo_offset = tune_fifo_offset self.sync_dly = sync_dly + self.gw_rev = -1 # discovered in init() self.dac_mmap = DAC34H84(dac).get_mmap() @@ -258,12 +263,10 @@ class Phaser: delay(.1*ms) # slack is_baseband = hw_rev & PHASER_HW_REV_VARIANT - gw_rev = self.read8(PHASER_ADDR_GW_REV) + self.gw_rev = self.read8(PHASER_ADDR_GW_REV) if debug: - print("gw_rev:", gw_rev) + print("gw_rev:", self.gw_rev) self.core.break_realtime() - is_base = gw_rev == 1 - is_miqro = gw_rev == 2 delay(.1*ms) # slack # allow a few errors during startup and alignment since boot @@ -384,7 +387,7 @@ class Phaser: channel.set_servo(profile=0, enable=0, hold=1) - if is_base: + if self.gw_rev == PHASER_GW_BASE: # test oscillators and DUC for i in range(len(channel.oscillator)): oscillator = channel.oscillator[i] @@ -412,7 +415,7 @@ class Phaser: abs(data_i - data_q) > 2): raise ValueError("DUC+oscillator phase/amplitude test failed") - if is_miqro: + if self.gw_rev == PHASER_GW_MIQRO: channel.miqro.reset() if is_baseband: diff --git a/artiq/frontend/artiq_ddb_template.py b/artiq/frontend/artiq_ddb_template.py index 503f4862e..5459756fe 100755 --- a/artiq/frontend/artiq_ddb_template.py +++ b/artiq/frontend/artiq_ddb_template.py @@ -560,6 +560,12 @@ class PeripheralManager: def process_phaser(self, rtio_offset, peripheral): mode = peripheral.get("mode", "base") + if mode == "miqro": + dac = ', "dac": {"pll_m": 16, "pll_n": 3, "interpolation": 2}' + n_channels = 3 + else: + dac = "" + n_channels = 5 self.gen(""" device_db["{name}"] = {{ "type": "local", @@ -567,15 +573,13 @@ class PeripheralManager: "class": "Phaser", "arguments": {{ "channel_base": 0x{channel:06x}, - "miso_delay": 1, - "mode": "{mode}" + "miso_delay": 1{dac} }} }}""", name=self.get_name("phaser"), - mode=mode, + dac=dac, channel=rtio_offset) - rtio_channels = {"base": 5, "miqro": 3}[mode] - return rtio_channels + return n_channels def process_hvamp(self, rtio_offset, peripheral): hvamp_name = self.get_name("hvamp") diff --git a/artiq/frontend/artiq_sinara_tester.py b/artiq/frontend/artiq_sinara_tester.py index a4f999029..fea05bbcd 100755 --- a/artiq/frontend/artiq_sinara_tester.py +++ b/artiq/frontend/artiq_sinara_tester.py @@ -570,20 +570,33 @@ class SinaraTester(EnvExperiment): self.core.break_realtime() phaser.init() delay(1*ms) - phaser.channel[0].set_duc_frequency(duc) - phaser.channel[0].set_duc_cfg() - phaser.channel[0].set_att(6*dB) - phaser.channel[1].set_duc_frequency(-duc) - phaser.channel[1].set_duc_cfg() - phaser.channel[1].set_att(6*dB) - phaser.duc_stb() - delay(1*ms) - for i in range(len(osc)): - phaser.channel[0].oscillator[i].set_frequency(osc[i]) - phaser.channel[0].oscillator[i].set_amplitude_phase(.2) - phaser.channel[1].oscillator[i].set_frequency(-osc[i]) - phaser.channel[1].oscillator[i].set_amplitude_phase(.2) + if phaser.gw_rev == 1: # base + phaser.channel[0].set_duc_frequency(duc) + phaser.channel[0].set_duc_cfg() + phaser.channel[0].set_att(6*dB) + phaser.channel[1].set_duc_frequency(-duc) + phaser.channel[1].set_duc_cfg() + phaser.channel[1].set_att(6*dB) + phaser.duc_stb() delay(1*ms) + for i in range(len(osc)): + phaser.channel[0].oscillator[i].set_frequency(osc[i]) + phaser.channel[0].oscillator[i].set_amplitude_phase(.2) + phaser.channel[1].oscillator[i].set_frequency(-osc[i]) + phaser.channel[1].oscillator[i].set_amplitude_phase(.2) + delay(1*ms) + elif phaser.gw_rev == 2: # miqro + for ch in range(2): + delay(1*ms) + phaser.channel[ch].set_att(6*dB) + phaser.channel[ch].miqro.set_window( + start=0x00, iq=[[1., 0.]], order=0, tail=0) + sign = 1. - 2.*ch + for i in range(len(osc)): + phaser.channel[ch].miqro.set_profile(osc, profile=1, + frequency=sign*(duc + osc[i]), amplitude=1./len(osc)) + phaser.channel[ch].miqro.pulse( + window=0x000, profiles=[1 for _ in range(len(osc))]) @kernel def phaser_led_wave(self, phasers): From 513f9f00f3cee687c3a82be9f3dec1fa74fdab11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 23 Sep 2022 12:59:21 +0000 Subject: [PATCH 42/72] miqro: document coredevice driver --- artiq/coredevice/phaser.py | 106 +++++++++++++++++++++++++++++++++---- 1 file changed, 97 insertions(+), 9 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 4afe0c992..6d0cefba3 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -1420,15 +1420,28 @@ class Miqro: @kernel def reset(self): + """Establish no-output profiles and no-output window and execute them. + + This establishes the first profile (index 0) on all oscillators as zero + amplitude, creates a trivial window (one sample with zero amplitude, + minimal interpolation), and executes a corresponding pulse. + """ for osc in range(16): - for pro in range(32): - self.set_profile_mu(osc, pro, 0, 0, 0) - delay(10*us) - self.set_window_mu(start=0, iq=[0], rate=1, shift=0, order=0, head=0, tail=0) + self.set_profile_mu(osc, profile=0, ftw=0, asf=0) + delay(10*us) + self.set_window_mu(start=0, iq=[0], order=0) self.pulse(window=0, profiles=[0]) @kernel - def set_profile_mu(self, oscillator, profile, ftw, asf, pow=0): + def set_profile_mu(self, oscillator, profile, ftw, asf, pow_=0): + """Store an oscillator profile (machine units). + + :param oscillator: Oscillator index (0 to 15) + :param profile: Profile index (0 to 31) + :param ftw: Frequency tuning word (32 bit signed integer on a 250 MHz clock) + :param asf: Amplitude scale factor (16 bit unsigned integer) + :param pow_: Phase offset word (16 bit integer) + """ if oscillator >= 16: raise ValueError("invalid oscillator index") if profile >= 32: @@ -1438,21 +1451,51 @@ class Miqro: (oscillator << 6) | (profile << 1)) self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, ftw) self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, - (asf & 0xffff) | (pow << 16)) + (asf & 0xffff) | (pow_ << 16)) @kernel def set_profile(self, oscillator, profile, frequency, amplitude, phase=0.): - # frequency is interpreted in the Nyquist sense, i.e. aliased + """Store an oscillator profile. + + :param oscillator: Oscillator index (0 to 15) + :param profile: Profile index (0 to 31) + :param frequency: Frequency in Hz (passband -100 to 100 MHz). + Interpreted in the Nyquist sense, i.e. aliased. + :param amplitude: Amplitude in units of full scale (0. to 1.) + :param phase: Phase in turns. See :class:`Miqro` for a definition of + phase in this context. + :return: The quantized 32 bit frequency tuning word + """ ftw = int32(round(frequency*((1 << 30)/(62.5*MHz)))) asf = int32(round(amplitude*0xffff)) if asf < 0 or asf > 0xffff: raise ValueError("amplitude out of bounds") - pow = int32(round(phase*(1 << 16))) - self.set_profile_mu(oscillator, profile, ftw, asf, pow) + pow_ = int32(round(phase*(1 << 16))) + self.set_profile_mu(oscillator, profile, ftw, asf, pow_) return ftw @kernel def set_window_mu(self, start, iq, rate=1, shift=0, order=3, head=1, tail=1): + """Store a window segment (machine units) + + :param start: Window start address (0 to 0x3ff) + :param iq: List of IQ window samples. Each window sample is an integer + containing the signed I part in the 16 LSB and the signed Q part in + the 16 MSB. The maximum window length is 0x3fe. The user must + ensure that this window does not overlap with other windows in the + memory. + :param rate: Interpolation rate change (1 to 1 << 12) + :param shift: Interpolator amplitude gain compensation in powers of 2 (0 to 63) + :param order: Interpolation order from 0 (corresponding to + constant/zero-order-hold/1st order CIC interpolation) to 3 (corresponding + to cubic/Parzen/4th order CIC interpolation) + :param head: Update the interpolator settings and clear its state at the start + of the window. This also implies starting the envelope from zero. + :param tail: Feed zeros into the interpolator after the window samples. + In the absence of further pulses this will return the output envelope + to zero with the chosen interpolation. + :return: Next available window memory address after this segment. + """ if start >= 1 << 10: raise ValueError("start out of bounds") if len(iq) >= 1 << 10: @@ -1480,6 +1523,24 @@ class Miqro: @kernel def set_window(self, start, iq, period=4*ns, order=3, head=1, tail=1): + """Store a window segment + + :param start: Window start address (0 to 0x3ff) + :param iq: List of IQ window samples. Each window sample is a pair of + two float numbers -1 to 1, one for each I and Q in units of full scale. + The maximum window length is 0x3fe. The user must ensure that this window + does not overlap with other windows in the memory. + :param period: Desired window sample period in SI units (4*ns to (4 << 12)*ns). + :param order: Interpolation order from 0 (corresponding to + constant/zero-order-hold/1st order CIC interpolation) to 3 (corresponding + to cubic/Parzen/4th order CIC interpolation) + :param head: Update the interpolator settings and clear its state at the start + of the window. This also implies starting the envelope from zero. + :param tail: Feed zeros into the interpolator after the window samples. + In the absence of further pulses this will return the output envelope + to zero with the chosen interpolation. + :return: Actual sample period in SI units + """ rate = int32(round(period/(4*ns))) gain = 1. for _ in range(order): @@ -1499,6 +1560,17 @@ class Miqro: @kernel def encode(self, window, profiles, data): + """Encode window and profile selection + + :param window: Window start address (0 to 0x3ff) + :param profiles: List of profile indices for the oscillators. Maximum + length 16. Unused oscillators will be set to profile 0. + :param data: List of integers to store the encoded data words into. + Unused entries will remain untouched. Must contain at least three + lements if all oscillators are used and should be initialized to + zeros. + :return: Number of words from `data` used. + """ if len(profiles) > 16: raise ValueError("too many oscillators") if window > 0x3ff: @@ -1518,6 +1590,13 @@ class Miqro: @kernel def pulse_mu(self, data): + """Emit a pulse (encoded) + + The pulse fiducial timing resolution is 4 ns. + + :param data: List of up to 3 words containing an encoded MIQRO pulse as + returned by :meth:`encode`. + """ word = len(data) delay_mu(-8*word) # back shift to align while word > 0: @@ -1528,6 +1607,15 @@ class Miqro: @kernel def pulse(self, window, profiles): + """Emit a pulse + + This encodes the window and profiles (see :meth:`encode`) and emits them + (see :meth:`pulse_mu`). + + :param window: Window start address (0 to 0x3ff) + :param profiles: List of profile indices for the oscillators. Maximum + length 16. Unused oscillators will select profile 0. + """ data = [0, 0, 0] words = self.encode(window, profiles, data) self.pulse_mu(data[:words]) From 740f3d220bfe2bd196d071373a787a0f4131a614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 23 Sep 2022 13:39:49 +0000 Subject: [PATCH 43/72] refine/fixes --- artiq/coredevice/phaser.py | 12 ++++++------ artiq/frontend/artiq_sinara_tester.py | 10 ++++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 6d0cefba3..8a7131c2d 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -383,7 +383,7 @@ class Phaser: if channel.get_att_mu() != 0x5a: raise ValueError("attenuator test failed") delay(.1*ms) - channel.set_att_mu(0x00) # minimum attenuation + channel.set_att_mu(0x00) # maximum attenuation channel.set_servo(profile=0, enable=0, hold=1) @@ -1487,8 +1487,8 @@ class Miqro: :param rate: Interpolation rate change (1 to 1 << 12) :param shift: Interpolator amplitude gain compensation in powers of 2 (0 to 63) :param order: Interpolation order from 0 (corresponding to - constant/zero-order-hold/1st order CIC interpolation) to 3 (corresponding - to cubic/Parzen/4th order CIC interpolation) + constant/rectangular window/zero-order-hold/1st order CIC interpolation) + to 3 (corresponding to cubic/Parzen window/4th order CIC interpolation) :param head: Update the interpolator settings and clear its state at the start of the window. This also implies starting the envelope from zero. :param tail: Feed zeros into the interpolator after the window samples. @@ -1581,12 +1581,12 @@ class Miqro: for profile in profiles: if profile > 0x1f: raise ValueError("profile out of bounds") - data[word] |= profile << idx - idx += 5 if idx > 32 - 5: word += 1 idx = 0 - return word + data[word] |= profile << idx + idx += 5 + return word + 1 @kernel def pulse_mu(self, data): diff --git a/artiq/frontend/artiq_sinara_tester.py b/artiq/frontend/artiq_sinara_tester.py index fea05bbcd..84a68c405 100755 --- a/artiq/frontend/artiq_sinara_tester.py +++ b/artiq/frontend/artiq_sinara_tester.py @@ -587,16 +587,18 @@ class SinaraTester(EnvExperiment): delay(1*ms) elif phaser.gw_rev == 2: # miqro for ch in range(2): - delay(1*ms) phaser.channel[ch].set_att(6*dB) - phaser.channel[ch].miqro.set_window( - start=0x00, iq=[[1., 0.]], order=0, tail=0) + phaser.channel[ch].set_duc_cfg(select=0) sign = 1. - 2.*ch for i in range(len(osc)): - phaser.channel[ch].miqro.set_profile(osc, profile=1, + phaser.channel[ch].miqro.set_profile(i, profile=1, frequency=sign*(duc + osc[i]), amplitude=1./len(osc)) + delay(100*us) + phaser.channel[ch].miqro.set_window( + start=0x000, iq=[[1., 0.]], order=0, tail=0) phaser.channel[ch].miqro.pulse( window=0x000, profiles=[1 for _ in range(len(osc))]) + delay(1*ms) @kernel def phaser_led_wave(self, phasers): From a0053f7a2b15d1842d2bbe20d1bab5e1bf5da90e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 23 Sep 2022 15:57:43 +0200 Subject: [PATCH 44/72] add release note --- RELEASE_NOTES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index 69c725337..d99a4c594 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -9,7 +9,8 @@ Unreleased Highlights: * Implemented Phaser-servo. This requires recent gateware on Phaser. - +* Implemented Phaser-MIQRO support. This requires the Phaser MIQRO gateware + variant. ARTIQ-7 ------- From a1a4545ed4935e52ed63bb7e1d38418df60615e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 23 Sep 2022 16:22:21 +0200 Subject: [PATCH 45/72] docs: fix syntax --- artiq/coredevice/phaser.py | 40 ++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 8a7131c2d..bc82c8751 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -92,14 +92,15 @@ class Phaser: that have different features. Phaser mode and coredevice PHY are both both selected at gateware compile-time and need to match. - Phaser gateware | Coredevice PHY | Features per :class:`PhaserChannel` - --------------- | -------------- | ----------------------------------- - Base <= v0.5 | Base | Base (5 :class:`PhaserOscillator`) - Base >= v0.6 | Base | Base + Servo - Miqro >= v0.6 | Miqro | :class:`Miqro` + =============== ============== =================================== + Phaser gateware Coredevice PHY Features per :class:`PhaserChannel` + =============== ============== =================================== + Base <= v0.5 Base Base (5 :class:`PhaserOscillator`) + Base >= v0.6 Base Base + Servo + Miqro >= v0.6 Miqro :class:`Miqro` + =============== ============== =================================== - Base mode - --------- + **Base mode** The coredevice produces 2 IQ (in-phase and quadrature) data streams with 25 MS/s and 14 bit per quadrature. Each data stream supports 5 independent @@ -131,15 +132,13 @@ class Phaser: absolute phase with respect to other RTIO input and output events (see `get_next_frame_mu()`). - Miqro mode - ---------- + **Miqro mode** See :class:`Miqro` Here the DAC operates in 4x interpolation. - Analog flow - ----------- + **Analog flow** The four analog DAC outputs are passed through anti-aliasing filters. @@ -158,8 +157,7 @@ class Phaser: configured through a shared SPI bus that is accessed and controlled via FPGA registers. - Servo - ----- + **Servo** 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 @@ -1193,8 +1191,8 @@ class PhaserChannel: 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. + 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): @@ -1323,8 +1321,7 @@ class Miqro: contained in the Phaser gateware. The output is generated by with the following data flow: - Oscillators - ........... + **Oscillators** * There are n_osc = 16 oscillators with oscillator IDs 0..n_osc-1. * Each oscillator outputs one tone at any given time @@ -1359,16 +1356,14 @@ class Miqro: during fast pulse sequences. They are intended for use in calibration and initialization. - Summation - ......... + **Summation** * The oscillator outputs are added together (wrapping addition). * The user must ensure that the sum of oscillators outputs does not exceed the data range. In general that means that the sum of the amplitudes must not exceed one. - Shaper - ...... + **Shaper** * The summed complex output stream is then multiplied with a the complex-valued output of a triggerable shaper. @@ -1393,8 +1388,7 @@ class Miqro: each window respectively. This is used to implement pulses with arbitrary length or CW output. - Overall properties - .................. + **Overall properties** * The DAC may upconvert the signal by applying a frequency offset f1 with phase p1. From b89584632225f8cf10e6557b001d80eee45f93b4 Mon Sep 17 00:00:00 2001 From: Michael Birtwell Date: Mon, 11 Jul 2022 11:26:20 +0100 Subject: [PATCH 46/72] Improve exception reports when exception can't be reconstructed Artiq assumes that all exceptions raised by the kernel can be constructed with a single string argument. This isn't always the case. Especially for exceptions that originated in python and were propagated to the kernel over rpc. With out this change a mosek solver failure looks like: ``` ERROR root:logging_tools.py:41 Terminating with exception (TypeError: __init__() missing 1 required positional argument: 'msg') Traceback (most recent call last): File "/home/mb/.cache/pypoetry/virtualenvs/ion-transport-1-b41LI0-py3.8/lib/python3.8/site-packages/artiq/master/worker_impl.py", line 540, in main exp_inst.run() File "/home/mb/.cache/pypoetry/virtualenvs/ion-transport-1-b41LI0-py3.8/lib/python3.8/site-packages/artiq/test_tools/experiment.py", line 82, in wrapper meth() File "/home/mb/.cache/pypoetry/virtualenvs/ion-transport-1-b41LI0-py3.8/lib/python3.8/site-packages/artiq/language/core.py", line 54, in run_on_core return getattr(self, arg).run(run_on_core, ((self,) + k_args), k_kwargs) File "/home/mb/.cache/pypoetry/virtualenvs/ion-transport-1-b41LI0-py3.8/lib/python3.8/site-packages/artiq/coredevice/core.py", line 152, in run self.comm.serve(embedding_map, symbolizer, demangler) File "/home/mb/.cache/pypoetry/virtualenvs/ion-transport-1-b41LI0-py3.8/lib/python3.8/site-packages/artiq/coredevice/comm_kernel.py", line 720, in serve self._serve_exception(embedding_map, symbolizer, demangler) File "/home/mb/.cache/pypoetry/virtualenvs/ion-transport-1-b41LI0-py3.8/lib/python3.8/site-packages/artiq/coredevice/comm_kernel.py", line 699, in _serve_exception python_exn = python_exn_type( TypeError: __init__() missing 1 required positional argument: 'msg' ``` With this change we get: ``` ERROR root:logging_tools.py:41 Terminating with exception (RuntimeError: Exception type=, which couldn't be reconstructed (__init__() missing 1 required positional argument: 'msg')) Core Device Traceback: Traceback (most recent call first): File "/home/mb/oxionics/ion-transport/tests/test_end_to_end.py", line 280, in get_transport return self.seq.solve() File "/home/mb/oxionics/ion-transport/tests/test_end_to_end.py", line 288, in artiq_worker_test_end_to_end.TransportTestScan.run(..., ...) (RA=+0x2e4) self.seq.record(self.get_transport(1e-6 + 1e-7 * x)) mosek.Error(27): rescode.err_license_expired(1001): The license has expired. End of Core Device Traceback Traceback (most recent call last): File "/home/mb/oxionics/artiq/artiq/master/worker_impl.py", line 540, in main exp_inst.run() File "/home/mb/oxionics/artiq/artiq/test_tools/experiment.py", line 82, in wrapper meth() File "/home/mb/oxionics/artiq/artiq/language/core.py", line 54, in run_on_core return getattr(self, arg).run(run_on_core, ((self,) + k_args), k_kwargs) File "/home/mb/oxionics/artiq/artiq/coredevice/core.py", line 152, in run self.comm.serve(embedding_map, symbolizer, demangler) File "/home/mb/oxionics/artiq/artiq/coredevice/comm_kernel.py", line 732, in serve self._serve_exception(embedding_map, symbolizer, demangler) File "/home/mb/oxionics/artiq/artiq/coredevice/comm_kernel.py", line 714, in _serve_exception raise python_exn RuntimeError: Exception type=, which couldn't be reconstructed (__init__() missing 1 required positional argument: 'msg') ``` Signed-off-by: Michael Birtwell --- artiq/coredevice/comm_kernel.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/artiq/coredevice/comm_kernel.py b/artiq/coredevice/comm_kernel.py index 930430b5e..3d5b8dea9 100644 --- a/artiq/coredevice/comm_kernel.py +++ b/artiq/coredevice/comm_kernel.py @@ -686,8 +686,14 @@ class CommKernel: else: python_exn_type = embedding_map.retrieve_object(core_exn.id) - python_exn = python_exn_type( - nested_exceptions[-1][1].format(*nested_exceptions[0][2])) + try: + python_exn = python_exn_type( + nested_exceptions[-1][1].format(*nested_exceptions[0][2])) + except Exception as ex: + python_exn = RuntimeError( + f"Exception type={python_exn_type}, which couldn't be " + f"reconstructed ({ex})" + ) python_exn.artiq_core_exception = core_exn raise python_exn From 56e6b1428c954108f61f395758c77e9f3517e33a Mon Sep 17 00:00:00 2001 From: fanmingyu212 Date: Fri, 30 Sep 2022 22:53:34 -0700 Subject: [PATCH 47/72] llvm: change addr2line to symbolizer `llvm-addr2line` is not included as part of the llvm binary package for Windows. This causes ARTIQ python compilations issues when conda is not used (so the `llvm-tools` conda package is not installed, which provides `llvm-addr2line` currently). --- artiq/compiler/targets.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/artiq/compiler/targets.py b/artiq/compiler/targets.py index f25d8f77d..e2e0c1b24 100644 --- a/artiq/compiler/targets.py +++ b/artiq/compiler/targets.py @@ -91,7 +91,7 @@ class Target: tool_ld = "ld.lld" tool_strip = "llvm-strip" - tool_addr2line = "llvm-addr2line" + tool_symbolizer = "llvm-symbolizer" tool_cxxfilt = "llvm-cxxfilt" def __init__(self): @@ -218,8 +218,8 @@ class Target: # the backtrace entry should point at. last_inlined = None offset_addresses = [hex(addr - 1) for addr in addresses] - with RunTool([self.tool_addr2line, "--addresses", "--functions", "--inlines", - "--demangle", "--exe={library}"] + offset_addresses, + with RunTool([self.tool_symbolizer, "--addresses", "--functions", "--inlines", + "--demangle", "--output-style=GNU", "--exe={library}"] + offset_addresses, library=library) \ as results: lines = iter(results["__stdout__"].read().rstrip().split("\n")) @@ -275,7 +275,7 @@ class RV32IMATarget(Target): tool_ld = "ld.lld" tool_strip = "llvm-strip" - tool_addr2line = "llvm-addr2line" + tool_symbolizer = "llvm-symbolizer" tool_cxxfilt = "llvm-cxxfilt" class RV32GTarget(Target): @@ -288,7 +288,7 @@ class RV32GTarget(Target): tool_ld = "ld.lld" tool_strip = "llvm-strip" - tool_addr2line = "llvm-addr2line" + tool_symbolizer = "llvm-symbolizer" tool_cxxfilt = "llvm-cxxfilt" class CortexA9Target(Target): @@ -301,5 +301,5 @@ class CortexA9Target(Target): tool_ld = "ld.lld" tool_strip = "llvm-strip" - tool_addr2line = "llvm-addr2line" + tool_symbolizer = "llvm-symbolizer" tool_cxxfilt = "llvm-cxxfilt" From 9846ee653c55117e41b42bbe22601430d176f487 Mon Sep 17 00:00:00 2001 From: wlph17 <55318763+wlph17@users.noreply.github.com> Date: Fri, 7 Oct 2022 11:31:43 +0800 Subject: [PATCH 48/72] flake: set Nix Qt environment variables in development shell allows applets to run standalone via ``python -m ...`` without requiring the Nix Qt wrapper --- flake.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flake.nix b/flake.nix index bc2cf378e..f08a88c1f 100644 --- a/flake.nix +++ b/flake.nix @@ -447,6 +447,8 @@ ]; shellHook = '' export LIBARTIQ_SUPPORT=`libartiq-support` + export QT_PLUGIN_PATH=${pkgs.qt5.qtbase}/${pkgs.qt5.qtbase.dev.qtPluginPrefix} + export QML2_IMPORT_PATH=${pkgs.qt5.qtbase}/${pkgs.qt5.qtbase.dev.qtQmlPrefix} ''; }; From 192cab887fb5e0e46b0f1f4de6d896ee54324385 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Fri, 7 Oct 2022 11:39:36 +0800 Subject: [PATCH 49/72] afws_client: update --- artiq/frontend/afws_client.py | 47 ++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/artiq/frontend/afws_client.py b/artiq/frontend/afws_client.py index e3a32187a..bf12f4759 100755 --- a/artiq/frontend/afws_client.py +++ b/artiq/frontend/afws_client.py @@ -84,10 +84,7 @@ class Client: def build(self, major_ver, rev, variant, log, experimental_features): if not variant: - variants = self.get_variants() - if len(variants) != 1: - raise ValueError("User can build more than 1 variant - need to specify") - variant = variants[0][0] + variant = self.get_single_variant(error_msg="User can build more than 1 variant - need to specify") print("Building variant: {}".format(variant)) build_args = ( rev, @@ -140,6 +137,26 @@ class Client: raise ValueError("Unexpected server reply: expected 'OK', got '{}'".format(reply)) return self.read_json() + def get_single_variant(self, error_msg): + variants = self.get_variants() + if len(variants) != 1: + print(error_msg) + table = PrettyTable() + table.field_names = ["Variant", "Expiry date"] + table.add_rows(variants) + print(table) + sys.exit(1) + return variants[0][0] + + def get_json(self, variant): + self.send_command("GET_JSON", variant) + reply = self.read_reply() + if reply[0] != "OK": + return reply[0], None + length = int(reply[1]) + json_str = self.fsocket.read(length).decode("ascii") + return "OK", json_str + def main(): parser = argparse.ArgumentParser() @@ -158,6 +175,10 @@ def main(): act_build.add_argument("variant", nargs="?", default=None, help="variant to build (can be omitted if user is authorised to build only one)") act_passwd = action.add_parser("passwd", help="change password") act_get_variants = action.add_parser("get_variants", help="get available variants and expiry dates") + act_get_json = action.add_parser("get_json", help="get JSON description file of variant") + act_get_json.add_argument("variant", nargs="?", default=None, help="variant to get (can be omitted if user is authorised to build only one)") + act_get_json.add_argument("-o", "--out", default=None, help="output JSON file") + act_get_json.add_argument("-f", "--force", action="store_true", help="overwrite file if it already exists") args = parser.parse_args() cert = args.cert @@ -228,6 +249,24 @@ def main(): table.field_names = ["Variant", "Expiry date"] table.add_rows(data) print(table) + elif args.action == "get_json": + if args.variant: + variant = args.variant + else: + variant = client.get_single_variant(error_msg="User can get JSON of more than 1 variant - need to specify") + result, json_str = client.get_json(variant) + if result != "OK": + if result == "UNAUTHORIZED": + print(f"You are not authorized to get JSON of variant {variant}. Your firmware subscription may have expired. Contact helpdesk\x40m-labs.hk.") + sys.exit(1) + if args.out: + if not args.force and os.path.exists(args.out): + print(f"File {args.out} already exists. You can use -f to overwrite the existing file.") + sys.exit(1) + with open(args.out, "w") as f: + f.write(json_str) + else: + print(json_str) else: raise ValueError finally: From 3ffbc5681ed1ecef7c1fd0f0c4d6c82630b5b31d Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Sat, 8 Oct 2022 13:31:52 +0800 Subject: [PATCH 50/72] flake: update dependencies, enable misoc tests --- flake.lock | 54 +++++++++++++++++++++++++++++++++++------------------- flake.nix | 1 - 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/flake.lock b/flake.lock index 7eaac721f..e243fa85d 100644 --- a/flake.lock +++ b/flake.lock @@ -2,6 +2,7 @@ "nodes": { "artiq-comtools": { "inputs": { + "flake-utils": "flake-utils", "nixpkgs": [ "nixpkgs" ], @@ -10,11 +11,11 @@ ] }, "locked": { - "lastModified": 1654007592, - "narHash": "sha256-vaDFhE1ItjqtIcinC/6RAJGbj44pxxMUEeQUa3FtgEE=", + "lastModified": 1664405593, + "narHash": "sha256-yP441NerlLGig7n+9xHsx8yCtZ+Ggd0VqfBSzc20E04=", "owner": "m-labs", "repo": "artiq-comtools", - "rev": "cb73281154656ee8f74db1866859e31bf42755cd", + "rev": "15ddac62813ef623a076ccf982b3bc63d314e651", "type": "github" }, "original": { @@ -23,14 +24,29 @@ "type": "github" } }, + "flake-utils": { + "locked": { + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "mozilla-overlay": { "flake": false, "locked": { - "lastModified": 1657214286, - "narHash": "sha256-rO/4oymKXU09wG2bcTt4uthPCp1XsBZjxuCJo3yVXNs=", + "lastModified": 1664789696, + "narHash": "sha256-UGWJHQShiwLCr4/DysMVFrYdYYHcOqAOVsWNUu+l6YU=", "owner": "mozilla", "repo": "nixpkgs-mozilla", - "rev": "0508a66e28a5792fdfb126bbf4dec1029c2509e0", + "rev": "80627b282705101e7b38e19ca6e8df105031b072", "type": "github" }, "original": { @@ -41,11 +57,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1657123678, - "narHash": "sha256-cowVkScfUPlbBXUp08MeVk/wgm9E1zp1uC+9no2hZYw=", + "lastModified": 1665066044, + "narHash": "sha256-mkO0LMHVunMFRWLcJhHT0fBf2v6RlH3vg7EVpfSIAFc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "316b762afdb9e142a803f29c49a88b4a47db80ee", + "rev": "ed9b904c5eba055a6d6f5c1ccb89ba8f0a056dc6", "type": "github" }, "original": { @@ -73,11 +89,11 @@ ] }, "locked": { - "lastModified": 1654830914, - "narHash": "sha256-tratXcWu6Dgzd0Qd9V6EMjuNlE9qDN1pKFhP+Gt0b64=", + "lastModified": 1664319253, + "narHash": "sha256-hycJAgy+NFF9f5I6++7yo8KdhMSyKCPKJazRPxeedI4=", "owner": "m-labs", "repo": "sipyco", - "rev": "58b0935f7ae47659abee5b5792fa594153328d6f", + "rev": "d58ded7280e0f020be2446d4fee70f4393e6045f", "type": "github" }, "original": { @@ -89,11 +105,11 @@ "src-migen": { "flake": false, "locked": { - "lastModified": 1656649178, - "narHash": "sha256-A91sZRrprEuPOtIUVxm6wX5djac9wnNZQ4+cU1nvJPc=", + "lastModified": 1662111470, + "narHash": "sha256-IPyhoFZLhY8d3jHB8jyvGdbey7V+X5eCzBZYSrJ18ec=", "owner": "m-labs", "repo": "migen", - "rev": "0fb91737090fe45fd764ea3f71257a4c53c7a4ae", + "rev": "639e66f4f453438e83d86dc13491b9403bbd8ec6", "type": "github" }, "original": { @@ -105,11 +121,11 @@ "src-misoc": { "flake": false, "locked": { - "lastModified": 1649324486, - "narHash": "sha256-Mw/fQS3lHFvCm7L1k63joRkz5uyijQfywcOq+X2+o2s=", + "lastModified": 1665206592, + "narHash": "sha256-w/fb2kk27QBCuDZ5IfMZbCOWYhZFqY4c4Ba4qGJWrPI=", "ref": "master", - "rev": "f1dc58d2b8c222ba41c25cee4301626625f46e43", - "revCount": 2420, + "rev": "6a7c670ab6120b8136f652c41d907eb0fb16ed54", + "revCount": 2426, "submodules": true, "type": "git", "url": "https://github.com/m-labs/misoc.git" diff --git a/flake.nix b/flake.nix index f08a88c1f..5a9f37c62 100644 --- a/flake.nix +++ b/flake.nix @@ -201,7 +201,6 @@ misoc = pkgs.python3Packages.buildPythonPackage { name = "misoc"; src = src-misoc; - doCheck = false; # TODO: fix misoc bitrot and re-enable tests propagatedBuildInputs = with pkgs.python3Packages; [ jinja2 numpy migen pyserial asyncserial ]; }; From 19b8d28a2e9543e7a2d8f3fc7abe574eea7fea4f Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 10 Oct 2022 17:58:17 +0800 Subject: [PATCH 51/72] flake: update dependencies --- flake.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/flake.lock b/flake.lock index e243fa85d..4ce86597c 100644 --- a/flake.lock +++ b/flake.lock @@ -57,11 +57,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1665066044, - "narHash": "sha256-mkO0LMHVunMFRWLcJhHT0fBf2v6RlH3vg7EVpfSIAFc=", + "lastModified": 1665132027, + "narHash": "sha256-zoHPqSQSENt96zTk6Mt1AP+dMNqQDshXKQ4I6MfjP80=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ed9b904c5eba055a6d6f5c1ccb89ba8f0a056dc6", + "rev": "9ecc270f02b09b2f6a76b98488554dd842797357", "type": "github" }, "original": { @@ -121,11 +121,11 @@ "src-misoc": { "flake": false, "locked": { - "lastModified": 1665206592, - "narHash": "sha256-w/fb2kk27QBCuDZ5IfMZbCOWYhZFqY4c4Ba4qGJWrPI=", + "lastModified": 1665395741, + "narHash": "sha256-7ULMGBPPn5NxZX6rdxU5GheoSNBiJklHQEVf04jU9tI=", "ref": "master", - "rev": "6a7c670ab6120b8136f652c41d907eb0fb16ed54", - "revCount": 2426, + "rev": "4fb0730db4c5de7e86f82fa3bd204e6c4608af85", + "revCount": 2427, "submodules": true, "type": "git", "url": "https://github.com/m-labs/misoc.git" From 286f151d9a97d97d782dece6777f553b81e3c188 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Wed, 19 Oct 2022 13:05:51 +0800 Subject: [PATCH 52/72] flake: switch to upstream llvmlite --- flake.lock | 6 +++--- flake.nix | 26 ++------------------------ 2 files changed, 5 insertions(+), 27 deletions(-) diff --git a/flake.lock b/flake.lock index 4ce86597c..d138f372c 100644 --- a/flake.lock +++ b/flake.lock @@ -57,11 +57,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1665132027, - "narHash": "sha256-zoHPqSQSENt96zTk6Mt1AP+dMNqQDshXKQ4I6MfjP80=", + "lastModified": 1666056570, + "narHash": "sha256-e7EkIY68Tp7NKyp9JSHh6CgPPdsKYYWxiL4wZQN8Cwg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9ecc270f02b09b2f6a76b98488554dd842797357", + "rev": "47edaa313fc3767ce3026037a5b62352f22f3602", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 5a9f37c62..4c5178a83 100644 --- a/flake.nix +++ b/flake.nix @@ -112,28 +112,6 @@ ''; }; - llvmlite-new = pkgs.python3Packages.buildPythonPackage rec { - pname = "llvmlite"; - version = "0.38.0"; - src = pkgs.python3Packages.fetchPypi { - inherit pname; - version = "0.38.0"; - sha256 = "qZ0WbM87EW87ntI7m3C6JBVkCpyXjzqqE/rUnFj0llw="; - }; - nativeBuildInputs = [ pkgs.llvm_11 ]; - # Disable static linking - # https://github.com/numba/llvmlite/issues/93 - postPatch = '' - substituteInPlace ffi/Makefile.linux --replace "-static-libstdc++" "" - substituteInPlace llvmlite/tests/test_binding.py --replace "test_linux" "nope" - ''; - # Set directory containing llvm-config binary - preConfigure = '' - export LLVM_CONFIG=${pkgs.llvm_11.dev}/bin/llvm-config - ''; - doCheck = false; # FIXME - }; - artiq-upstream = pkgs.python3Packages.buildPythonPackage rec { pname = "artiq"; version = artiqVersion; @@ -147,8 +125,8 @@ nativeBuildInputs = [ pkgs.qt5.wrapQtAppsHook ]; # keep llvm_x and lld_x in sync with llvmlite - propagatedBuildInputs = [ pkgs.llvm_11 pkgs.lld_11 llvmlite-new sipyco.packages.x86_64-linux.sipyco pythonparser artiq-comtools.packages.x86_64-linux.artiq-comtools ] - ++ (with pkgs.python3Packages; [ pyqtgraph pygit2 numpy dateutil scipy prettytable pyserial python-Levenshtein h5py pyqt5 qasync tqdm ]); + propagatedBuildInputs = [ pkgs.llvm_11 pkgs.lld_11 sipyco.packages.x86_64-linux.sipyco pythonparser artiq-comtools.packages.x86_64-linux.artiq-comtools ] + ++ (with pkgs.python3Packages; [ llvmlite pyqtgraph pygit2 numpy dateutil scipy prettytable pyserial python-Levenshtein h5py pyqt5 qasync tqdm ]); dontWrapQtApps = true; postFixup = '' From d5e267fadf8f2d33871ae273c5796282c7af6aa3 Mon Sep 17 00:00:00 2001 From: Fabian Schwartau Date: Wed, 19 Oct 2022 15:45:45 +0200 Subject: [PATCH 53/72] Fixed two too low delay values in Phaser init Signed-off-by: Fabian Schwartau --- artiq/coredevice/phaser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 9fad1d964..162db8b23 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -275,7 +275,7 @@ class Phaser: for data in self.dac_mmap: self.dac_write(data >> 16, data) - delay(40*us) + delay(120*us) self.dac_sync() delay(40*us) @@ -662,7 +662,7 @@ class Phaser: .. note:: Synchronising the NCO clears the phase-accumulator """ config1f = self.dac_read(0x1f) - delay(.1*ms) + delay(.4*ms) self.dac_write(0x1f, config1f & ~int32(1 << 1)) self.dac_write(0x1f, config1f | (1 << 1)) From 1820e1f71537bef2cb829b942138983b0d7fd482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Wed, 19 Oct 2022 16:25:33 +0200 Subject: [PATCH 54/72] phaser: cleanup --- artiq/frontend/artiq_sinara_tester.py | 9 ++++++--- artiq/gateware/rtio/phy/phaser.py | 1 - 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/artiq/frontend/artiq_sinara_tester.py b/artiq/frontend/artiq_sinara_tester.py index 84a68c405..919588547 100755 --- a/artiq/frontend/artiq_sinara_tester.py +++ b/artiq/frontend/artiq_sinara_tester.py @@ -8,6 +8,7 @@ import sys from artiq.experiment import * from artiq.coredevice.ad9910 import AD9910, SyncDataEeprom +from artiq.coredevice.phaser import PHASER_GW_BASE, PHASER_GW_MIQRO from artiq.master.databases import DeviceDB from artiq.master.worker_db import DeviceManager @@ -86,7 +87,7 @@ class SinaraTester(EnvExperiment): elif (module, cls) == ("artiq.coredevice.fastino", "Fastino"): self.fastinos[name] = self.get_device(name) elif (module, cls) == ("artiq.coredevice.phaser", "Phaser"): - self.phasers[name] = self.get_device(name) + self. import PHASER_GW_BASE, PHASER_GW_MIQROphasers[name] = self.get_device(name) elif (module, cls) == ("artiq.coredevice.grabber", "Grabber"): self.grabbers[name] = self.get_device(name) elif (module, cls) == ("artiq.coredevice.mirny", "Mirny"): @@ -570,7 +571,7 @@ class SinaraTester(EnvExperiment): self.core.break_realtime() phaser.init() delay(1*ms) - if phaser.gw_rev == 1: # base + if phaser.gw_rev == PHASER_GW_BASE: # base phaser.channel[0].set_duc_frequency(duc) phaser.channel[0].set_duc_cfg() phaser.channel[0].set_att(6*dB) @@ -585,7 +586,7 @@ class SinaraTester(EnvExperiment): phaser.channel[1].oscillator[i].set_frequency(-osc[i]) phaser.channel[1].oscillator[i].set_amplitude_phase(.2) delay(1*ms) - elif phaser.gw_rev == 2: # miqro + elif phaser.gw_rev == PHASER_GW_BASE: # miqro for ch in range(2): phaser.channel[ch].set_att(6*dB) phaser.channel[ch].set_duc_cfg(select=0) @@ -599,6 +600,8 @@ class SinaraTester(EnvExperiment): phaser.channel[ch].miqro.pulse( window=0x000, profiles=[1 for _ in range(len(osc))]) delay(1*ms) + else: + raise ValueError @kernel def phaser_led_wave(self, phasers): diff --git a/artiq/gateware/rtio/phy/phaser.py b/artiq/gateware/rtio/phy/phaser.py index 3c19d8956..557a65d74 100644 --- a/artiq/gateware/rtio/phy/phaser.py +++ b/artiq/gateware/rtio/phy/phaser.py @@ -1,6 +1,5 @@ from migen import * from misoc.cores.duc import MultiDDS -from misoc.interconnect.stream import Endpoint from artiq.gateware.rtio import rtlink from .fastlink import SerDes, SerInterface From e15b5b50d8ac9340d38a3a251112ac7c9874ad23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Wed, 19 Oct 2022 16:42:03 +0200 Subject: [PATCH 55/72] phaser: tweak docs, relax slack --- artiq/coredevice/phaser.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index bc82c8751..c474e3744 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -89,8 +89,8 @@ class Phaser: quadrature modulation compensation and interpolation features. The coredevice RTIO PHY and the Phaser gateware come in different modes - that have different features. Phaser mode and coredevice PHY are both - both selected at gateware compile-time and need to match. + that have different features. Phaser mode and coredevice PHY mode are both + selected at their respective gateware compile-time and need to match. =============== ============== =================================== Phaser gateware Coredevice PHY Features per :class:`PhaserChannel` @@ -100,6 +100,12 @@ class Phaser: Miqro >= v0.6 Miqro :class:`Miqro` =============== ============== =================================== + The coredevice driver (this class and :class:`PhaserChannel`) exposes + the superset of all functionality regardless of the Coredevice RTIO PHY + or Phaser gateware modes. This is to evade type unification limitations. + Features absent in Coredevice PHY/Phaser gateware will not work and + should not be accessed. + **Base mode** The coredevice produces 2 IQ (in-phase and quadrature) data streams with 25 @@ -833,8 +839,9 @@ class PhaserChannel: A Phaser channel contains: - * multiple oscillators (in the coredevice phy), + * multiple :class:`PhaserOscillator` (in the coredevice phy), * an interpolation chain and digital upconverter (DUC) on Phaser, + * a :class:`Miqro` instance on Phaser, * several channel-specific settings in the DAC: * quadrature modulation compensation QMC @@ -846,6 +853,7 @@ class PhaserChannel: Attributes: * :attr:`oscillator`: List of five :class:`PhaserOscillator`. + * :attr:`miqro`: A :class:`Miqro`. .. note:: The amplitude sum of the oscillators must be less than one to avoid clipping or overflow. If any of the DDS or DUC frequencies are @@ -858,6 +866,8 @@ class PhaserChannel: changes in oscillator parameters, the overshoot can lead to clipping or overflow after the interpolation. Either band-limit any changes in the oscillator parameters or back off the amplitude sufficiently. + Miqro is not affected by this. But both the oscillators and Miqro can + be affected by intrinsic overshoot of the interpolator on the DAC. """ kernel_invariants = {"index", "phaser", "trf_mmap"} @@ -1422,7 +1432,7 @@ class Miqro: """ for osc in range(16): self.set_profile_mu(osc, profile=0, ftw=0, asf=0) - delay(10*us) + delay(20*us) self.set_window_mu(start=0, iq=[0], order=0) self.pulse(window=0, profiles=[0]) @@ -1512,7 +1522,7 @@ class Miqro: ) for iqi in iq: self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, iqi) - delay(10*us) # slack for long windows + delay(20*us) # slack for long windows return (start + 1 + len(iq)) & 0x3ff @kernel From eb7a0714b36986a6b253809e4d21ad1642b9d8bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Wed, 19 Oct 2022 16:42:56 +0200 Subject: [PATCH 56/72] literal copy paste error --- artiq/frontend/artiq_sinara_tester.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/artiq/frontend/artiq_sinara_tester.py b/artiq/frontend/artiq_sinara_tester.py index 919588547..5da93aefc 100755 --- a/artiq/frontend/artiq_sinara_tester.py +++ b/artiq/frontend/artiq_sinara_tester.py @@ -87,7 +87,7 @@ class SinaraTester(EnvExperiment): elif (module, cls) == ("artiq.coredevice.fastino", "Fastino"): self.fastinos[name] = self.get_device(name) elif (module, cls) == ("artiq.coredevice.phaser", "Phaser"): - self. import PHASER_GW_BASE, PHASER_GW_MIQROphasers[name] = self.get_device(name) + self.phasers[name] = self.get_device(name) elif (module, cls) == ("artiq.coredevice.grabber", "Grabber"): self.grabbers[name] = self.get_device(name) elif (module, cls) == ("artiq.coredevice.mirny", "Mirny"): @@ -589,7 +589,7 @@ class SinaraTester(EnvExperiment): elif phaser.gw_rev == PHASER_GW_BASE: # miqro for ch in range(2): phaser.channel[ch].set_att(6*dB) - phaser.channel[ch].set_duc_cfg(select=0) + phaser.channel[ch].set_duc_cfg() sign = 1. - 2.*ch for i in range(len(osc)): phaser.channel[ch].miqro.set_profile(i, profile=1, From 07db7704239e87eee378548814984d745ff1d54a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Wed, 19 Oct 2022 16:54:00 +0200 Subject: [PATCH 57/72] phaser: fix tester --- artiq/frontend/artiq_sinara_tester.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/artiq/frontend/artiq_sinara_tester.py b/artiq/frontend/artiq_sinara_tester.py index 5da93aefc..aeb7f3720 100755 --- a/artiq/frontend/artiq_sinara_tester.py +++ b/artiq/frontend/artiq_sinara_tester.py @@ -571,7 +571,7 @@ class SinaraTester(EnvExperiment): self.core.break_realtime() phaser.init() delay(1*ms) - if phaser.gw_rev == PHASER_GW_BASE: # base + if phaser.gw_rev == PHASER_GW_BASE: phaser.channel[0].set_duc_frequency(duc) phaser.channel[0].set_duc_cfg() phaser.channel[0].set_att(6*dB) @@ -586,7 +586,7 @@ class SinaraTester(EnvExperiment): phaser.channel[1].oscillator[i].set_frequency(-osc[i]) phaser.channel[1].oscillator[i].set_amplitude_phase(.2) delay(1*ms) - elif phaser.gw_rev == PHASER_GW_BASE: # miqro + elif phaser.gw_rev == PHASER_GW_MIQRO: for ch in range(2): phaser.channel[ch].set_att(6*dB) phaser.channel[ch].set_duc_cfg() From e0b1098bc07cc20d97ae28d6325edacc9679a4c0 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Fri, 21 Oct 2022 19:12:58 +0800 Subject: [PATCH 58/72] dashboard: remove incorrect moninj proxy message --- artiq/frontend/artiq_dashboard.py | 1 - 1 file changed, 1 deletion(-) diff --git a/artiq/frontend/artiq_dashboard.py b/artiq/frontend/artiq_dashboard.py index f620d8617..3b0d3c17d 100755 --- a/artiq/frontend/artiq_dashboard.py +++ b/artiq/frontend/artiq_dashboard.py @@ -250,7 +250,6 @@ def main(): server_description = args.server logging.info("ARTIQ dashboard version: %s", artiq_version) - logging.info("ARTIQ dashboard connected to moninj_proxy (%s)", server_description) # run main_window.show() loop.run_until_complete(main_window.exit_request.wait()) From f75ddf78b0da79a56627334c498e84206dda1f6b Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Fri, 21 Oct 2022 19:16:37 +0800 Subject: [PATCH 59/72] dashboard: restore connection/version message --- artiq/frontend/artiq_dashboard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/artiq/frontend/artiq_dashboard.py b/artiq/frontend/artiq_dashboard.py index 3b0d3c17d..dfe1e82d6 100755 --- a/artiq/frontend/artiq_dashboard.py +++ b/artiq/frontend/artiq_dashboard.py @@ -248,8 +248,8 @@ def main(): server_description = server_name + " ({})".format(args.server) else: server_description = args.server - logging.info("ARTIQ dashboard version: %s", - artiq_version) + logging.info("ARTIQ dashboard %s connected to master %s", + artiq_version, server_description) # run main_window.show() loop.run_until_complete(main_window.exit_request.wait()) From f3f068036ae93f25587630c387bb34487f29e86c Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Thu, 3 Nov 2022 21:24:49 +0800 Subject: [PATCH 60/72] use maintained fork of python-Levenshtein --- flake.nix | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 4c5178a83..a00f41016 100644 --- a/flake.nix +++ b/flake.nix @@ -126,7 +126,7 @@ nativeBuildInputs = [ pkgs.qt5.wrapQtAppsHook ]; # keep llvm_x and lld_x in sync with llvmlite propagatedBuildInputs = [ pkgs.llvm_11 pkgs.lld_11 sipyco.packages.x86_64-linux.sipyco pythonparser artiq-comtools.packages.x86_64-linux.artiq-comtools ] - ++ (with pkgs.python3Packages; [ llvmlite pyqtgraph pygit2 numpy dateutil scipy prettytable pyserial python-Levenshtein h5py pyqt5 qasync tqdm ]); + ++ (with pkgs.python3Packages; [ llvmlite pyqtgraph pygit2 numpy dateutil scipy prettytable pyserial levenshtein h5py pyqt5 qasync tqdm ]); dontWrapQtApps = true; postFixup = '' diff --git a/setup.py b/setup.py index 733caf239..09159d80d 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ requirements = [ "numpy", "scipy", "python-dateutil", "prettytable", "h5py", "qasync", "pyqtgraph", "pygit2", - "llvmlite", "pythonparser", "python-Levenshtein", + "llvmlite", "pythonparser", "levenshtein", ] console_scripts = [ From e2178f6c865fa9ee4f948630a8ab3028a989b7b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=AB=E7=84=9A=20=E5=AF=8C=E8=89=AF?= Date: Wed, 9 Nov 2022 16:55:17 +0800 Subject: [PATCH 61/72] Fix GUI log issues introduced by #1950 --- artiq/frontend/artiq_browser.py | 2 +- artiq/gui/log.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/artiq/frontend/artiq_browser.py b/artiq/frontend/artiq_browser.py index fc6d25fe2..204ac2382 100755 --- a/artiq/frontend/artiq_browser.py +++ b/artiq/frontend/artiq_browser.py @@ -153,7 +153,7 @@ def main(): browser = Browser(smgr, datasets_sub, args.browse_root, args.server, args.port) - widget_log_handler.callback = browser.log.append_message + widget_log_handler.callback = browser.log.model.append if os.name == "nt": # HACK: show the main window before creating applets. diff --git a/artiq/gui/log.py b/artiq/gui/log.py index 6a8ddecad..3fd05d80d 100644 --- a/artiq/gui/log.py +++ b/artiq/gui/log.py @@ -209,11 +209,11 @@ class LogDock(QDockWidgetCloseDetect): grid.addWidget(QtWidgets.QLabel("Minimum level: "), 0, 0) self.filter_level = QtWidgets.QComboBox() self.filter_level.addItems(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]) - self.filter_level.setToolTip("Receive entries at or above this level") + self.filter_level.setToolTip("Filter entries at or above this level") grid.addWidget(self.filter_level, 0, 1) self.filter_freetext = QtWidgets.QLineEdit() self.filter_freetext.setPlaceholderText("freetext filter...") - self.filter_freetext.setToolTip("Receive entries containing this text") + self.filter_freetext.setToolTip("Filter entries containing this text") grid.addWidget(self.filter_freetext, 0, 2) scrollbottom = QtWidgets.QToolButton() From defc69d9c377e34c3b192de70d83c10f1955790f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=AB=E7=84=9A=20=E5=AF=8C=E8=89=AF?= Date: Fri, 11 Nov 2022 13:15:50 +0800 Subject: [PATCH 62/72] compiler: fix const str/bytes handling (#1990) --- artiq/compiler/transforms/llvm_ir_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/artiq/compiler/transforms/llvm_ir_generator.py b/artiq/compiler/transforms/llvm_ir_generator.py index 881a9e717..cd54c372f 100644 --- a/artiq/compiler/transforms/llvm_ir_generator.py +++ b/artiq/compiler/transforms/llvm_ir_generator.py @@ -335,8 +335,8 @@ class LLVMIRGenerator: else: value = const.value - llptr = self.llstr_of_str(const.value, linkage="private", unnamed_addr=True) - lllen = ll.Constant(lli32, len(const.value)) + llptr = self.llstr_of_str(value, linkage="private", unnamed_addr=True) + lllen = ll.Constant(lli32, len(value)) return ll.Constant(llty, (llptr, lllen)) else: assert False From beff15de5e57c18fdecec35b79ba71f73e29fca4 Mon Sep 17 00:00:00 2001 From: David Nadlinger Date: Fri, 4 Nov 2022 18:16:19 +0000 Subject: [PATCH 63/72] compiler/targets: Fix refactoring leftover for native (host) target It's unclear whether this actually caused any issues, or why this wasn't done before (instead just setting the now-removed endianness flag). --- artiq/compiler/targets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artiq/compiler/targets.py b/artiq/compiler/targets.py index e2e0c1b24..dcaa823df 100644 --- a/artiq/compiler/targets.py +++ b/artiq/compiler/targets.py @@ -263,7 +263,7 @@ class NativeTarget(Target): def __init__(self): super().__init__() self.triple = llvm.get_default_triple() - host_data_layout = str(llvm.targets.Target.from_default_triple().create_target_machine().target_data) + self.data_layout = str(llvm.targets.Target.from_default_triple().create_target_machine().target_data) class RV32IMATarget(Target): triple = "riscv32-unknown-linux" From 00a27b105a4750e7bd7c590d1b54a607a63fccfb Mon Sep 17 00:00:00 2001 From: David Nadlinger Date: Wed, 9 Nov 2022 21:01:24 +0000 Subject: [PATCH 64/72] compiler: Extract maximum alignment from target data layout In particular, i64/double are actually supposed to be aligned to their size on RISC-V (at least according to the ELF psABI), though it is unclear to me whether this actually caused any issues. --- artiq/compiler/transforms/llvm_ir_generator.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/artiq/compiler/transforms/llvm_ir_generator.py b/artiq/compiler/transforms/llvm_ir_generator.py index cd54c372f..e3a554cf3 100644 --- a/artiq/compiler/transforms/llvm_ir_generator.py +++ b/artiq/compiler/transforms/llvm_ir_generator.py @@ -177,6 +177,15 @@ class LLVMIRGenerator: self.empty_metadata = self.llmodule.add_metadata([]) self.quote_fail_msg = None + # Maximum alignment required according to the target platform ABI. As this is + # not directly exposed by LLVM, just take the maximum across all the "big" + # elementary types we use. (Vector types, should we ever support them, are + # likely contenders for even larger alignment requirements.) + self.max_target_alignment = max(map( + lambda t: self.abi_layout_info.get_size_align(t)[1], + [lli64, lldouble, llptr] + )) + def add_pred(self, pred, block): if block not in self.llpred_map: self.llpred_map[block] = set() @@ -1529,7 +1538,7 @@ class LLVMIRGenerator: self.llbuilder.position_at_end(llalloc) llalloca = self.llbuilder.alloca(lli8, llsize, name="rpc.alloc") - llalloca.align = 4 # maximum alignment required by OR1K ABI + llalloca.align = self.max_target_alignment llphi.add_incoming(llalloca, llalloc) self.llbuilder.branch(llhead) From 4819016a3c485328067fe3e5745196c6933b8f9b Mon Sep 17 00:00:00 2001 From: David Nadlinger Date: Wed, 9 Nov 2022 21:06:14 +0000 Subject: [PATCH 65/72] firmware/ksupport: Document rpc_recv alignment requirements [nfc] --- artiq/firmware/ksupport/lib.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/artiq/firmware/ksupport/lib.rs b/artiq/firmware/ksupport/lib.rs index 2e7346c19..d9786ae3c 100644 --- a/artiq/firmware/ksupport/lib.rs +++ b/artiq/firmware/ksupport/lib.rs @@ -156,6 +156,21 @@ extern fn rpc_send_async(service: u32, tag: &CSlice, data: *const *const ()) }) } + +/// Receives the result from an RPC call into the given memory buffer. +/// +/// To handle aggregate objects with an a priori unknown size and number of +/// sub-allocations (e.g. a list of list of lists, where, at each level, the number of +/// elements is not statically known), this function needs to be called in a loop: +/// +/// On the first call, `slot` should be a buffer of suitable size and alignment for +/// the top-level return value (e.g. in the case of a list, the pointer/length pair). +/// A return value of zero indicates that the value has been completely received. +/// As long as the return value is positive, another allocation with the given number of +/// bytes is needed, so the function should be called again with such a buffer (aligned +/// to the maximum required for any of the possible types according to the target ABI). +/// +/// If the RPC call resulted in an exception, it is reconstructed and raised. #[unwind(allowed)] extern fn rpc_recv(slot: *mut ()) -> usize { send(&RpcRecvRequest(slot)); From 6caa779c74e6d1d42f367bf54713eed326a68fc5 Mon Sep 17 00:00:00 2001 From: David Nadlinger Date: Sun, 13 Nov 2022 20:07:25 +0000 Subject: [PATCH 66/72] firmware/ksupport: Include .gcc_except_table (LSDA) For whatever reason, no language-specific unwind data was generated for ksupport code so far, but rustc does emit it for an upcoming refactoring. --- artiq/firmware/ksupport/ksupport.ld | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/artiq/firmware/ksupport/ksupport.ld b/artiq/firmware/ksupport/ksupport.ld index 23e84a781..66b159f13 100644 --- a/artiq/firmware/ksupport/ksupport.ld +++ b/artiq/firmware/ksupport/ksupport.ld @@ -60,6 +60,11 @@ SECTIONS KEEP(*(.eh_frame_hdr)) } > ksupport :text :eh_frame + .gcc_except_table : + { + *(.gcc_except_table) + } > ksupport + .data : { *(.data .data.*) From 8740ec3dd52d85084237797881ea137492bfe070 Mon Sep 17 00:00:00 2001 From: David Nadlinger Date: Thu, 10 Nov 2022 02:03:47 +0000 Subject: [PATCH 67/72] firmware/rpc_proto: Fix size/alignment calculation for structs with tail padding Also factors out duplicate code for (de)serializing elements of lists and ndarrays, and replaces the rounding calculations by the well-known, much faster power-of-two-only bit-twiddling version. GitHub: Fixes #1934. --- artiq/firmware/libproto_artiq/rpc_proto.rs | 280 +++++++++++---------- artiq/test/coredevice/test_embedding.py | 34 ++- 2 files changed, 174 insertions(+), 140 deletions(-) diff --git a/artiq/firmware/libproto_artiq/rpc_proto.rs b/artiq/firmware/libproto_artiq/rpc_proto.rs index 5750d524e..983d932b8 100644 --- a/artiq/firmware/libproto_artiq/rpc_proto.rs +++ b/artiq/firmware/libproto_artiq/rpc_proto.rs @@ -6,22 +6,81 @@ use io::{ProtoRead, Read, Write, ProtoWrite, Error}; use self::tag::{Tag, TagIterator, split_tag}; #[inline] -fn alignment_offset(alignment: isize, ptr: isize) -> isize { - (-ptr).rem_euclid(alignment) +fn round_up(val: usize, power_of_two: usize) -> usize { + assert!(power_of_two.is_power_of_two()); + let max_rem = power_of_two - 1; + (val + max_rem) & (!max_rem) } +#[inline] +unsafe fn round_up_mut(ptr: *mut T, power_of_two: usize) -> *mut T { + round_up(ptr as usize, power_of_two) as *mut T +} + +#[inline] +unsafe fn round_up_const(ptr: *const T, power_of_two: usize) -> *const T { + round_up(ptr as usize, power_of_two) as *const T +} + +#[inline] unsafe fn align_ptr(ptr: *const ()) -> *const T { - let alignment = core::mem::align_of::() as isize; - let fix = alignment_offset(alignment as isize, ptr as isize); - ((ptr as isize) + fix) as *const T + round_up_const(ptr, core::mem::align_of::()) as *const T } +#[inline] unsafe fn align_ptr_mut(ptr: *mut ()) -> *mut T { - let alignment = core::mem::align_of::() as isize; - let fix = alignment_offset(alignment as isize, ptr as isize); - ((ptr as isize) + fix) as *mut T + round_up_mut(ptr, core::mem::align_of::()) as *mut T } +/// Reads (deserializes) `length` array or list elements of type `tag` from `reader`, +/// writing them into the buffer given by `storage`. +/// +/// `alloc` is used for nested allocations (if elements themselves contain +/// lists/arrays), see [recv_value]. +unsafe fn recv_elements( + reader: &mut R, + tag: Tag, + length: usize, + storage: *mut (), + alloc: &dyn Fn(usize) -> Result<*mut (), E>, +) -> Result<(), E> +where + R: Read + ?Sized, + E: From>, +{ + // List of simple types are special-cased in the protocol for performance. + match tag { + Tag::Bool => { + let dest = slice::from_raw_parts_mut(storage as *mut u8, length); + reader.read_exact(dest)?; + }, + Tag::Int32 => { + let dest = slice::from_raw_parts_mut(storage as *mut u8, length * 4); + reader.read_exact(dest)?; + let dest = slice::from_raw_parts_mut(storage as *mut i32, length); + NativeEndian::from_slice_i32(dest); + }, + Tag::Int64 | Tag::Float64 => { + let dest = slice::from_raw_parts_mut(storage as *mut u8, length * 8); + reader.read_exact(dest)?; + let dest = slice::from_raw_parts_mut(storage as *mut i64, length); + NativeEndian::from_slice_i64(dest); + }, + _ => { + let mut data = storage; + for _ in 0..length { + recv_value(reader, tag, &mut data, alloc)? + } + } + } + Ok(()) +} + +/// Reads (deserializes) a value of type `tag` from `reader`, writing the results to +/// the kernel-side buffer `data` (the passed pointer to which is incremented to point +/// past the just-received data). For nested allocations (lists/arrays), `alloc` is +/// invoked any number of times with the size of the required allocation as a parameter +/// (which is assumed to be correctly aligned for all payload types). unsafe fn recv_value(reader: &mut R, tag: Tag, data: &mut *mut (), alloc: &dyn Fn(usize) -> Result<*mut (), E>) -> Result<(), E> @@ -59,99 +118,63 @@ unsafe fn recv_value(reader: &mut R, tag: Tag, data: &mut *mut (), }) } Tag::Tuple(it, arity) => { - *data = data.offset(alignment_offset(tag.alignment() as isize, *data as isize)); + let alignment = tag.alignment(); + *data = round_up_mut(*data, alignment); let mut it = it.clone(); for _ in 0..arity { let tag = it.next().expect("truncated tag"); recv_value(reader, tag, data, alloc)? } + // Take into account any tail padding (if element(s) with largest alignment + // are not at the end). + *data = round_up_mut(*data, alignment); Ok(()) } Tag::List(it) => { #[repr(C)] - struct List { elements: *mut (), length: u32 } - consume_value!(*mut List, |ptr| { + struct List { elements: *mut (), length: usize } + consume_value!(*mut List, |ptr_to_list| { let tag = it.clone().next().expect("truncated tag"); - let padding = if let Tag::Int64 | Tag::Float64 = tag { 4 } else { 0 }; - let length = reader.read_u32()? as usize; - let data = alloc(tag.size() * length + padding + 8)? as *mut u8; - *ptr = data as *mut List; - let ptr = data as *mut List; - let mut data = data.offset(8 + alignment_offset(tag.alignment() as isize, data as isize)) as *mut (); - (*ptr).length = length as u32; - (*ptr).elements = data; - match tag { - Tag::Bool => { - let dest = slice::from_raw_parts_mut(data as *mut u8, length); - reader.read_exact(dest)?; - }, - Tag::Int32 => { - let dest = slice::from_raw_parts_mut(data as *mut u8, length * 4); - reader.read_exact(dest)?; - let dest = slice::from_raw_parts_mut(data as *mut i32, length); - NativeEndian::from_slice_i32(dest); - }, - Tag::Int64 | Tag::Float64 => { - let dest = slice::from_raw_parts_mut(data as *mut u8, length * 8); - reader.read_exact(dest)?; - let dest = slice::from_raw_parts_mut(data as *mut i64, length); - NativeEndian::from_slice_i64(dest); - }, - _ => { - for _ in 0..length { - recv_value(reader, tag, &mut data, alloc)? - } - } - } - Ok(()) + // To avoid multiple kernel CPU roundtrips, use a single allocation for + // both the pointer/length List (slice) and the backing storage for the + // elements. We can assume that alloc() is aligned suitably, so just + // need to take into account any extra padding required. + // (Note: On RISC-V, there will never actually be any types with + // alignment larger than 8 bytes, so storage_offset == 0 always.) + let list_size = 4 + 4; + let storage_offset = round_up(list_size, tag.alignment()); + let storage_size = tag.size() * length; + + let allocation = alloc(storage_offset as usize + storage_size)? as *mut u8; + *ptr_to_list = allocation as *mut List; + let storage = allocation.offset(storage_offset as isize) as *mut (); + + (**ptr_to_list).length = length; + (**ptr_to_list).elements = storage; + recv_elements(reader, tag, length, storage, alloc) }) } Tag::Array(it, num_dims) => { consume_value!(*mut (), |buffer| { - let mut total_len: u32 = 1; + // Deserialize length along each dimension and compute total number of + // elements. + let mut total_len: usize = 1; for _ in 0..num_dims { - let len = reader.read_u32()?; + let len = reader.read_u32()? as usize; total_len *= len; - consume_value!(u32, |ptr| *ptr = len ) + consume_value!(usize, |ptr| *ptr = len ) } - let length = total_len as usize; + // Allocate backing storage for elements; deserialize them. let elt_tag = it.clone().next().expect("truncated tag"); - let padding = if let Tag::Int64 | Tag::Float64 = tag { 4 } else { 0 }; - let mut data = alloc(elt_tag.size() * length + padding)?; - data = data.offset(alignment_offset(tag.alignment() as isize, data as isize)); - - *buffer = data; - match elt_tag { - Tag::Bool => { - let dest = slice::from_raw_parts_mut(data as *mut u8, length); - reader.read_exact(dest)?; - }, - Tag::Int32 => { - let dest = slice::from_raw_parts_mut(data as *mut u8, length * 4); - reader.read_exact(dest)?; - let dest = slice::from_raw_parts_mut(data as *mut i32, length); - NativeEndian::from_slice_i32(dest); - }, - Tag::Int64 | Tag::Float64 => { - let dest = slice::from_raw_parts_mut(data as *mut u8, length * 8); - reader.read_exact(dest)?; - let dest = slice::from_raw_parts_mut(data as *mut i64, length); - NativeEndian::from_slice_i64(dest); - }, - _ => { - for _ in 0..length { - recv_value(reader, elt_tag, &mut data, alloc)? - } - } - } - Ok(()) + *buffer = alloc(elt_tag.size() * total_len)?; + recv_elements(reader, tag, total_len, *buffer, alloc) }) } Tag::Range(it) => { - *data = data.offset(alignment_offset(tag.alignment() as isize, *data as isize)); + *data = round_up_mut(*data, tag.alignment()); let tag = it.clone().next().expect("truncated tag"); recv_value(reader, tag, data, alloc)?; recv_value(reader, tag, data, alloc)?; @@ -180,6 +203,36 @@ pub fn recv_return(reader: &mut R, tag_bytes: &[u8], data: *mut (), Ok(()) } +unsafe fn send_elements(writer: &mut W, elt_tag: Tag, length: usize, data: *const ()) + -> Result<(), Error> + where W: Write + ?Sized +{ + writer.write_u8(elt_tag.as_u8())?; + match elt_tag { + // we cannot use NativeEndian::from_slice_i32 as the data is not mutable, + // and that is not needed as the data is already in native endian + Tag::Bool => { + let slice = slice::from_raw_parts(data as *const u8, length); + writer.write_all(slice)?; + }, + Tag::Int32 => { + let slice = slice::from_raw_parts(data as *const u8, length * 4); + writer.write_all(slice)?; + }, + Tag::Int64 | Tag::Float64 => { + let slice = slice::from_raw_parts(data as *const u8, length * 8); + writer.write_all(slice)?; + }, + _ => { + let mut data = data; + for _ in 0..length { + send_value(writer, elt_tag, &mut data)?; + } + } + } + Ok(()) +} + unsafe fn send_value(writer: &mut W, tag: Tag, data: &mut *const ()) -> Result<(), Error> where W: Write + ?Sized @@ -213,10 +266,13 @@ unsafe fn send_value(writer: &mut W, tag: Tag, data: &mut *const ()) Tag::Tuple(it, arity) => { let mut it = it.clone(); writer.write_u8(arity)?; + let mut max_alignment = 0; for _ in 0..arity { let tag = it.next().expect("truncated tag"); + max_alignment = core::cmp::max(max_alignment, tag.alignment()); send_value(writer, tag, data)? } + *data = round_up_const(*data, max_alignment); Ok(()) } Tag::List(it) => { @@ -226,30 +282,7 @@ unsafe fn send_value(writer: &mut W, tag: Tag, data: &mut *const ()) let length = (**ptr).length as usize; writer.write_u32((**ptr).length)?; let tag = it.clone().next().expect("truncated tag"); - let mut data = (**ptr).elements; - writer.write_u8(tag.as_u8())?; - match tag { - // we cannot use NativeEndian::from_slice_i32 as the data is not mutable, - // and that is not needed as the data is already in native endian - Tag::Bool => { - let slice = slice::from_raw_parts(data as *const u8, length); - writer.write_all(slice)?; - }, - Tag::Int32 => { - let slice = slice::from_raw_parts(data as *const u8, length * 4); - writer.write_all(slice)?; - }, - Tag::Int64 | Tag::Float64 => { - let slice = slice::from_raw_parts(data as *const u8, length * 8); - writer.write_all(slice)?; - }, - _ => { - for _ in 0..length { - send_value(writer, tag, &mut data)?; - } - } - } - Ok(()) + send_elements(writer, tag, length, (**ptr).elements) }) } Tag::Array(it, num_dims) => { @@ -265,30 +298,7 @@ unsafe fn send_value(writer: &mut W, tag: Tag, data: &mut *const ()) }) } let length = total_len as usize; - let mut data = *buffer; - writer.write_u8(elt_tag.as_u8())?; - match elt_tag { - // we cannot use NativeEndian::from_slice_i32 as the data is not mutable, - // and that is not needed as the data is already in native endian - Tag::Bool => { - let slice = slice::from_raw_parts(data as *const u8, length); - writer.write_all(slice)?; - }, - Tag::Int32 => { - let slice = slice::from_raw_parts(data as *const u8, length * 4); - writer.write_all(slice)?; - }, - Tag::Int64 | Tag::Float64 => { - let slice = slice::from_raw_parts(data as *const u8, length * 8); - writer.write_all(slice)?; - }, - _ => { - for _ in 0..length { - send_value(writer, elt_tag, &mut data)?; - } - } - } - Ok(()) + send_elements(writer, elt_tag, length, *buffer) }) } Tag::Range(it) => { @@ -349,7 +359,7 @@ pub fn send_args(writer: &mut W, service: u32, tag_bytes: &[u8], data: *const mod tag { use core::fmt; - use super::alignment_offset; + use super::round_up; pub fn split_tag(tag_bytes: &[u8]) -> (&[u8], &[u8]) { let tag_separator = @@ -416,16 +426,18 @@ mod tag { let it = it.clone(); it.take(3).map(|t| t.alignment()).max().unwrap() } - // CSlice basically - Tag::Bytes | Tag::String | Tag::ByteArray | Tag::List(_) => + // the ptr/length(s) pair is basically CSlice + Tag::Bytes | Tag::String | Tag::ByteArray | Tag::List(_) | Tag::Array(_, _) => core::mem::align_of::>(), - // array buffer is allocated, so no need for alignment first - Tag::Array(_, _) => 1, // will not be sent from the host _ => unreachable!("unexpected tag from host") } } + /// Returns the "alignment size" of a value with the type described by the tag + /// (in bytes), i.e. the stride between successive elements in a list/array of + /// the given type, or the offset from a struct element of this type to the + /// next field. pub fn size(self) -> usize { match self { Tag::None => 0, @@ -438,12 +450,18 @@ mod tag { Tag::ByteArray => 8, Tag::Tuple(it, arity) => { let mut size = 0; + let mut max_alignment = 0; let mut it = it.clone(); for _ in 0..arity { let tag = it.next().expect("truncated tag"); + let alignment = tag.alignment(); + max_alignment = core::cmp::max(max_alignment, alignment); + size = round_up(size, alignment); size += tag.size(); - size += alignment_offset(tag.alignment() as isize, size as isize) as usize; } + // Take into account any tail padding (if element(s) with largest + // alignment are not at the end). + size = round_up(size, max_alignment); size } Tag::List(_) => 8, diff --git a/artiq/test/coredevice/test_embedding.py b/artiq/test/coredevice/test_embedding.py index 3a6498933..537cec113 100644 --- a/artiq/test/coredevice/test_embedding.py +++ b/artiq/test/coredevice/test_embedding.py @@ -78,7 +78,10 @@ class RoundtripTest(ExperimentCase): self.assertRoundtrip(([1, 2], [3, 4])) def test_list_mixed_tuple(self): - self.assertRoundtrip([(0x12345678, [("foo", [0.0, 1.0], [0, 1])])]) + self.assertRoundtrip([ + (0x12345678, [("foo", [0.0, 1.0], [0, 1])]), + (0x23456789, [("bar", [2.0, 3.0], [2, 3])])]) + self.assertRoundtrip([(0, 1.0, 0), (1, 1.5, 2), (2, 1.9, 4)]) def test_array_1d(self): self.assertArrayRoundtrip(numpy.array([True, False])) @@ -520,19 +523,32 @@ class NumpyBoolTest(ExperimentCase): class _Alignment(EnvExperiment): def build(self): self.setattr_device("core") + self.a = False + self.b = 1234.5678 + self.c = True + self.d = True + self.e = 2345.6789 + self.f = False @rpc - def a_tuple(self) -> TList(TTuple([TBool, TFloat, TBool])): - return [(True, 1234.5678, True)] + def get_tuples(self) -> TList(TTuple([TBool, TFloat, TBool])): + return [(self.a, self.b, self.c), (self.d, self.e, self.f)] @kernel def run(self): - a, b, c = self.a_tuple()[0] - d, e, f = self.a_tuple()[0] - assert a == d - assert b == e - assert c == f - return 0 + # Run two RPCs before checking to catch any obvious allocation size calculation + # issues (i.e. use of uninitialised stack memory). + tuples0 = self.get_tuples() + tuples1 = self.get_tuples() + for tuples in [tuples0, tuples1]: + a, b, c = tuples[0] + d, e, f = tuples[1] + assert a == self.a + assert b == self.b + assert c == self.c + assert d == self.d + assert e == self.e + assert f == self.f class AlignmentTest(ExperimentCase): From dbbe8e8ed4f852e623775b7bd3aec818cdd03376 Mon Sep 17 00:00:00 2001 From: David Nadlinger Date: Mon, 14 Nov 2022 22:49:45 +0000 Subject: [PATCH 68/72] firmware/rpc_proto: Fix typo breaking receiving of arrays This was introduced in 8740ec3dd52d85084237797881ea137492bfe070. --- artiq/firmware/libproto_artiq/rpc_proto.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artiq/firmware/libproto_artiq/rpc_proto.rs b/artiq/firmware/libproto_artiq/rpc_proto.rs index 983d932b8..6b0a914a8 100644 --- a/artiq/firmware/libproto_artiq/rpc_proto.rs +++ b/artiq/firmware/libproto_artiq/rpc_proto.rs @@ -170,7 +170,7 @@ unsafe fn recv_value(reader: &mut R, tag: Tag, data: &mut *mut (), // Allocate backing storage for elements; deserialize them. let elt_tag = it.clone().next().expect("truncated tag"); *buffer = alloc(elt_tag.size() * total_len)?; - recv_elements(reader, tag, total_len, *buffer, alloc) + recv_elements(reader, elt_tag, total_len, *buffer, alloc) }) } Tag::Range(it) => { From 3d25092cbd7db5d4532df10d8d6494a490423f36 Mon Sep 17 00:00:00 2001 From: David Nadlinger Date: Mon, 14 Nov 2022 22:50:38 +0000 Subject: [PATCH 69/72] firmware/rpc_proto: Remove unnecessary cast [nfc] --- artiq/firmware/libproto_artiq/rpc_proto.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artiq/firmware/libproto_artiq/rpc_proto.rs b/artiq/firmware/libproto_artiq/rpc_proto.rs index 6b0a914a8..85fc902ba 100644 --- a/artiq/firmware/libproto_artiq/rpc_proto.rs +++ b/artiq/firmware/libproto_artiq/rpc_proto.rs @@ -147,7 +147,7 @@ unsafe fn recv_value(reader: &mut R, tag: Tag, data: &mut *mut (), let storage_offset = round_up(list_size, tag.alignment()); let storage_size = tag.size() * length; - let allocation = alloc(storage_offset as usize + storage_size)? as *mut u8; + let allocation = alloc(storage_offset + storage_size)? as *mut u8; *ptr_to_list = allocation as *mut List; let storage = allocation.offset(storage_offset as isize) as *mut (); From 404f24af6b3d996ad8149cb11ae2218af5fa6173 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Tue, 15 Nov 2022 11:20:06 +0800 Subject: [PATCH 70/72] compiler: set lld emulation explicitly --- artiq/compiler/targets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/artiq/compiler/targets.py b/artiq/compiler/targets.py index dcaa823df..df55e6bd8 100644 --- a/artiq/compiler/targets.py +++ b/artiq/compiler/targets.py @@ -269,7 +269,7 @@ class RV32IMATarget(Target): triple = "riscv32-unknown-linux" data_layout = "e-m:e-p:32:32-i64:64-n32-S128" features = ["m", "a"] - additional_linker_options = [] + additional_linker_options = ["-m", "elf32lriscv"] print_function = "core_log" now_pinning = True @@ -282,7 +282,7 @@ class RV32GTarget(Target): triple = "riscv32-unknown-linux" data_layout = "e-m:e-p:32:32-i64:64-n32-S128" features = ["m", "a", "f", "d"] - additional_linker_options = [] + additional_linker_options = ["-m", "elf32lriscv"] print_function = "core_log" now_pinning = True @@ -295,7 +295,7 @@ class CortexA9Target(Target): triple = "armv7-unknown-linux-gnueabihf" data_layout = "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64" features = ["dsp", "fp16", "neon", "vfp3"] - additional_linker_options = ["--target2=rel"] + additional_linker_options = ["-m", "armelf_linux_eabi", "--target2=rel"] print_function = "core_log" now_pinning = False From 2fe02cee6f9488758c1d25f6db420ab66669e1b0 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Tue, 15 Nov 2022 19:32:06 +0800 Subject: [PATCH 71/72] doc: MSYS2 packages --- CONTRIBUTING.rst | 1 - README.rst | 2 +- RELEASE_NOTES.rst | 5 +++-- doc/manual/installing.rst | 35 +++++++++++++++++++++++++---------- doc/manual/list_of_ndsps.rst | 10 ++++++---- 5 files changed, 35 insertions(+), 18 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 88100ddf5..0cdc9266e 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -26,7 +26,6 @@ report if possible: * Operating System * ARTIQ version (with recent versions of ARTIQ, run ``artiq_client --version``) * Version of the gateware and runtime loaded in the core device (in the output of ``artiq_coremgmt -D .... log``) - * If using Conda, output of `conda list` * Hardware involved diff --git a/README.rst b/README.rst index 216c77ba1..cef2fb286 100644 --- a/README.rst +++ b/README.rst @@ -13,7 +13,7 @@ ARTIQ uses FPGA hardware to perform its time-critical tasks. The `Sinara hardwar ARTIQ is designed to be portable to hardware platforms from different vendors and FPGA manufacturers. Several different configurations of a `FPGA evaluation kit `_ and of a `Zynq evaluation kit `_ are also used and supported. FPGA platforms can be combined with any number of additional peripherals, either already accessible from ARTIQ or made accessible with little effort. -ARTIQ and its dependencies are available in the form of Nix packages (for Linux) and Conda packages (for Windows and Linux). See `the manual `_ for installation instructions. +ARTIQ and its dependencies are available in the form of Nix packages (for Linux) and MSYS2 packages (for Windows). See `the manual `_ for installation instructions. Packages containing pre-compiled binary images to be loaded onto the hardware platforms are supplied for each configuration. Like any open source software ARTIQ can equally be built and installed directly from `source `_. diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index d99a4c594..28e06bdd6 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -3,14 +3,15 @@ Release notes ============= -Unreleased ----------- +ARTIQ-8 (Unreleased) +-------------------- Highlights: * Implemented Phaser-servo. This requires recent gateware on Phaser. * Implemented Phaser-MIQRO support. This requires the Phaser MIQRO gateware variant. +* MSYS2 packaging for Windows. ARTIQ-7 ------- diff --git a/doc/manual/installing.rst b/doc/manual/installing.rst index 8effbf2d9..19fe706a8 100644 --- a/doc/manual/installing.rst +++ b/doc/manual/installing.rst @@ -1,13 +1,7 @@ Installing ARTIQ ================ -ARTIQ can be installed using the Nix (on Linux) or Conda (on Windows or Linux) package managers. - -Nix is an innovative, robust, fast, and high-quality solution that comes with a larger collection of packages and features than Conda. However, Windows support is poor (using it with Windows Subsystem for Linux still has many problems) and Nix can be harder to learn. - -Conda has a more traditional approach to package management, is much more limited, slow, and lower-quality than Nix, but it supports Windows and it is simpler to use when it functions correctly. - -In the current state of affairs, we recommend that Linux users install ARTIQ via Nix and Windows users install it via Conda. +ARTIQ can be installed using the Nix (on Linux) or MSYS2 (on Windows) package managers. Using Conda is also possible on both platforms but not recommended. .. _installing-nix-users: @@ -90,11 +84,27 @@ You can create directories containing each a ``flake.nix`` that correspond to di If your favorite package is not available with Nix, contact us using the helpdesk@ email. -Installing via Conda (Windows, Linux) -------------------------------------- +Installing via MSYS2 (Windows) +------------------------------ + +Install `MSYS2 `_, then edit ``C:\MINGW64\etc\pacman.conf`` and add at the end: :: + + [artiq] + SigLevel = Optional TrustAll + Server = https://msys2.m-labs.hk/artiq-beta + +Launch ``MSYS2 MINGW64`` from the Windows Start menu to open the MSYS2 shell, and enter the following commands: :: + + pacman -Syy + pacman -S mingw-w64-x86_64-artiq + +If your favorite package is not available with MSYS2, contact us using the helpdesk@ email. + +Installing via Conda (Windows, Linux) [DEPRECATED] +-------------------------------------------------- .. warning:: - For Linux users, the Nix package manager is preferred, as it is more reliable and faster than Conda. + Installing ARTIQ via Conda is not recommended. Instead, Linux users should install it via Nix and Windows users should install it via MSYS2. Conda support may be removed in future ARTIQ releases and M-Labs can only provide very limited technical support for Conda. First, install `Anaconda `_ or the more minimalistic `Miniconda `_. @@ -133,6 +143,11 @@ To rollback to the previous version, respectively use ``$ nix profile rollback`` You may need to reflash the gateware and firmware of the core device to keep it synchronized with the software. +Upgrading ARTIQ (with MSYS2) +---------------------------- + +Run ``pacman -Syu`` to update all MSYS2 packages including ARTIQ. If you get a message telling you that the shell session must be restarted after a partial update, open the shell again after the partial update and repeat the command. See the MSYS2 and Pacman manual for information on how to update individual packages if required. + Upgrading ARTIQ (with Conda) ---------------------------- diff --git a/doc/manual/list_of_ndsps.rst b/doc/manual/list_of_ndsps.rst index 12d698e20..65953485c 100644 --- a/doc/manual/list_of_ndsps.rst +++ b/doc/manual/list_of_ndsps.rst @@ -4,7 +4,7 @@ List of available NDSPs The following network device support packages are available for ARTIQ. If you would like to add yours to this list, just send us an email or a pull request. +---------------------------------+-----------------------------------+----------------------------------+-----------------------------------------------------------------------------------------------------+--------------------------------------------------------+ -| Equipment | Nix package | Conda package | Documentation | URL | +| Equipment | Nix package | MSYS2 package | Documentation | URL | +=================================+===================================+==================================+=====================================================================================================+========================================================+ | PDQ2 | Not available | Not available | `HTML `_ | https://github.com/m-labs/pdq | +---------------------------------+-----------------------------------+----------------------------------+-----------------------------------------------------------------------------------------------------+--------------------------------------------------------+ @@ -20,11 +20,13 @@ The following network device support packages are available for ARTIQ. If you wo +---------------------------------+-----------------------------------+----------------------------------+-----------------------------------------------------------------------------------------------------+--------------------------------------------------------+ | Princeton Instruments PICam | Not available | Not available | Not available | https://github.com/quartiq/picam | +---------------------------------+-----------------------------------+----------------------------------+-----------------------------------------------------------------------------------------------------+--------------------------------------------------------+ -| Anel HUT2 power distribution | ``hut2`` | ``hut2`` | `HTML `_ | https://github.com/quartiq/hut2 | +| Anel HUT2 power distribution | ``hut2`` | Not available | `HTML `_ | https://github.com/quartiq/hut2 | +---------------------------------+-----------------------------------+----------------------------------+-----------------------------------------------------------------------------------------------------+--------------------------------------------------------+ -| TOPTICA lasers | ``toptica-lasersdk-artiq`` | ``toptica-lasersdk-artiq`` | Not available | https://github.com/quartiq/lasersdk-artiq | +| TOPTICA lasers | ``toptica-lasersdk-artiq`` | Not available | Not available | https://github.com/quartiq/lasersdk-artiq | +---------------------------------+-----------------------------------+----------------------------------+-----------------------------------------------------------------------------------------------------+--------------------------------------------------------+ -| HighFinesse wavemeters | ``highfinesse-net`` | ``highfinesse-net`` | `HTML `_ | https://github.com/quartiq/highfinesse-net | +| HighFinesse wavemeters | ``highfinesse-net`` | Not available | `HTML `_ | https://github.com/quartiq/highfinesse-net | +---------------------------------+-----------------------------------+----------------------------------+-----------------------------------------------------------------------------------------------------+--------------------------------------------------------+ | InfluxDB database | Not available | Not available | `HTML `_ | https://gitlab.com/charlesbaynham/artiq_influx_generic | +---------------------------------+-----------------------------------+----------------------------------+-----------------------------------------------------------------------------------------------------+--------------------------------------------------------+ + +MSYS2 packages all start with the ``mingw-w64-x86_64-`` prefix. From d45f9b6950b1a6e09b0910d2223d06293b50256d Mon Sep 17 00:00:00 2001 From: Etienne Wodey Date: Wed, 16 Nov 2022 19:45:28 +0100 Subject: [PATCH 72/72] ddb_template: propagate fastino log2_width setting Signed-off-by: Etienne Wodey --- artiq/frontend/artiq_ddb_template.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/artiq/frontend/artiq_ddb_template.py b/artiq/frontend/artiq_ddb_template.py index 5459756fe..275c6e42e 100755 --- a/artiq/frontend/artiq_ddb_template.py +++ b/artiq/frontend/artiq_ddb_template.py @@ -552,10 +552,11 @@ class PeripheralManager: "type": "local", "module": "artiq.coredevice.fastino", "class": "Fastino", - "arguments": {{"channel": 0x{channel:06x}}} + "arguments": {{"channel": 0x{channel:06x}, "log2_width": {log2_width}}} }}""", name=self.get_name("fastino"), - channel=rtio_offset) + channel=rtio_offset, + log2_width=peripheral["log2_width"]) return 1 def process_phaser(self, rtio_offset, peripheral):