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 01/34] 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 02/34] 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 03/34] 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 04/34] 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 05/34] 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 06/34] 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 07/34] 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 08/34] 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 09/34] 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 10/34] 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 11/34] 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 12/34] 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 13/34] 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 14/34] 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 15/34] 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 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 16/34] 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 17/34] 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 18/34] 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 19/34] 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 20/34] 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 21/34] 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 22/34] 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 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 23/34] 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 24/34] 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 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 25/34] 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 26/34] 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 27/34] 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 28/34] 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 29/34] 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 30/34] 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 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 31/34] 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 32/34] 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 33/34] 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 34/34] 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()