Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend SUServo to variable number of Urukul cards #1782

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
7 changes: 4 additions & 3 deletions RELEASE_NOTES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Highlights:
repository when building the list of experiments.
* The configuration entry ``rtio_clock`` supports multiple clocking settings, deprecating the usage
of compile-time options.
* Support for variable numbers of Urukul cards on SUServo.

Breaking changes:

Expand All @@ -38,6 +39,9 @@ Breaking changes:
* Phaser: fixed coarse mixer frequency configuration
* Mirny: Added extra delays in ``ADF5356.sync()``. This avoids the need of an extra delay before
calling `ADF5356.init()`.
* To support variable numbers of Urukul cards, the
``artiq.coredevice.suservo.SUServo`` constructor now accepts two device name lists,
``cpld_devices`` and ``dds_devices``, rather than four individual arguments.


ARTIQ-6
Expand Down Expand Up @@ -104,9 +108,6 @@ Breaking changes:
* ``quamash`` has been replaced with ``qasync``.
* Protocols are updated to use device endian.
* Analyzer dump format includes a byte for device endianness.
* To support variable numbers of Urukul cards in the future, the
``artiq.coredevice.suservo.SUServo`` constructor now accepts two device name lists,
``cpld_devices`` and ``dds_devices``, rather than four individual arguments.
Comment on lines -107 to -109
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be removed from the ARTIQ-6 release notes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was a mistake - these changes were not yet part of c224827 (ARTIQ-6 tag). Sorry for missing this in the last PR.

* Experiment classes with underscore-prefixed names are now ignored when ``artiq_client``
determines which experiment to submit (consistent with ``artiq_run``).

Expand Down
86 changes: 63 additions & 23 deletions artiq/coredevice/suservo.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,14 @@
from artiq.coredevice.rtio import rtio_output, rtio_input_data
from artiq.coredevice import spi2 as spi
from artiq.coredevice import urukul, sampler
from math import ceil, log2


COEFF_WIDTH = 18
COEFF_WIDTH = 18 # Must match gateware IIRWidths.coeff
Y_FULL_SCALE_MU = (1 << (COEFF_WIDTH - 1)) - 1
COEFF_DEPTH = 10 + 1
WE = 1 << COEFF_DEPTH + 1
STATE_SEL = 1 << COEFF_DEPTH
CONFIG_SEL = 1 << COEFF_DEPTH - 1
CONFIG_ADDR = CONFIG_SEL | STATE_SEL
T_CYCLE = (2*(8 + 64) + 2)*8*ns # Must match gateware Servo.t_cycle.
COEFF_SHIFT = 11
COEFF_SHIFT = 11 # Must match gateware IIRWidths.shift
PROFILE_WIDTH = 5 # Must match gateware IIRWidths.profile


@portable
Expand All @@ -35,8 +32,8 @@ class SUServo:
"""Sampler-Urukul Servo parent and configuration device.

Sampler-Urukul Servo is a integrated device controlling one
8-channel ADC (Sampler) and two 4-channel DDS (Urukuls) with a DSP engine
connecting the ADC data and the DDS output amplitudes to enable
8-channel ADC (Sampler) and any number of 4-channel DDS (Urukuls) with a
DSP engine connecting the ADC data and the DDS output amplitudes to enable
feedback. SU Servo can for example be used to implement intensity
stabilization of laser beams with an amplifier and AOM driven by Urukul
and a photodetector connected to Sampler.
Expand All @@ -49,7 +46,7 @@ class SUServo:
* See the SU Servo variant of the Kasli target for an example of how to
connect the gateware and the devices. Sampler and each Urukul need
two EEM connections.
* Ensure that both Urukuls are AD9910 variants and have the on-board
* Ensure that all Urukuls are AD9910 variants and have the on-board
dip switches set to 1100 (first two on, last two off).
* Refer to the Sampler and Urukul documentation and the SU Servo
example device database for runtime configuration of the devices
Expand All @@ -65,7 +62,8 @@ class SUServo:
:param core_device: Core device name
"""
kernel_invariants = {"channel", "core", "pgia", "cplds", "ddses",
"ref_period_mu"}
"ref_period_mu", "num_channels", "coeff_sel",
"state_sel", "config_addr", "write_enable"}

def __init__(self, dmgr, channel, pgia_device,
cpld_devices, dds_devices,
Expand All @@ -83,9 +81,19 @@ def __init__(self, dmgr, channel, pgia_device,
self.core.coarse_ref_period)
assert self.ref_period_mu == self.core.ref_multiplier

# The width of parts of the servo memory address depends on the number
# of channels.
self.num_channels = 4 * len(dds_devices)
channel_width = ceil(log2(self.num_channels))
coeff_depth = PROFILE_WIDTH + channel_width + 3
self.state_sel = 2 << (coeff_depth - 2)
self.config_addr = 3 << (coeff_depth - 2)
self.coeff_sel = 1 << coeff_depth
self.write_enable = 1 << (coeff_depth + 1)

@kernel
def init(self):
"""Initialize the servo, Sampler and both Urukuls.
"""Initialize the servo, Sampler and all Urukuls.

Leaves the servo disabled (see :meth:`set_config`), resets and
configures all DDS.
Expand Down Expand Up @@ -122,7 +130,7 @@ def write(self, addr, value):
:param addr: Memory location address.
:param value: Data to be written.
"""
addr |= WE
addr |= self.write_enable
value &= (1 << COEFF_WIDTH) - 1
value |= (addr >> 8) << COEFF_WIDTH
addr = addr & 0xff
Expand Down Expand Up @@ -158,7 +166,7 @@ def set_config(self, enable):
Disabling takes up to two servo cycles (~2.3 µs) to clear the
processing pipeline.
"""
self.write(CONFIG_ADDR, enable)
self.write(self.config_addr, enable)

@kernel
def get_status(self):
Expand All @@ -179,7 +187,7 @@ def get_status(self):
:return: Status. Bit 0: enabled, bit 1: done,
bits 8-15: channel clip indicators.
"""
return self.read(CONFIG_ADDR)
return self.read(self.config_addr)

@kernel
def get_adc_mu(self, adc):
Expand All @@ -197,7 +205,8 @@ def get_adc_mu(self, adc):
# State memory entries are 25 bits. Due to the pre-adder dynamic
# range, X0/X1/OFFSET are only 24 bits. Finally, the RTIO interface
# only returns the 18 MSBs (the width of the coefficient memory).
return self.read(STATE_SEL | (adc << 1) | (1 << 8))
return self.read(self.state_sel |
(2 * adc + (1 << PROFILE_WIDTH) * self.num_channels))

@kernel
def set_pgia_mu(self, channel, gain):
Expand Down Expand Up @@ -285,10 +294,11 @@ def set_dds_mu(self, profile, ftw, offs, pow_=0):
:param offs: IIR offset (17 bit signed)
:param pow_: Phase offset word (16 bit)
"""
base = (self.servo_channel << 8) | (profile << 3)
base = self.servo.coeff_sel | (self.servo_channel <<
(3 + PROFILE_WIDTH)) | (profile << 3)
self.servo.write(base + 0, ftw >> 16)
self.servo.write(base + 6, (ftw & 0xffff))
self.set_dds_offset_mu(profile, offs)
self.servo.write(base + 4, offs)
self.servo.write(base + 2, pow_)

@kernel
Expand Down Expand Up @@ -319,7 +329,8 @@ def set_dds_offset_mu(self, profile, offs):
:param profile: Profile number (0-31)
:param offs: IIR offset (17 bit signed)
"""
base = (self.servo_channel << 8) | (profile << 3)
base = self.servo.coeff_sel | (self.servo_channel <<
(3 + PROFILE_WIDTH)) | (profile << 3)
self.servo.write(base + 4, offs)

@kernel
Expand All @@ -344,6 +355,30 @@ def dds_offset_to_mu(self, offset):
"""
return int(round(offset * (1 << COEFF_WIDTH - 1)))

@kernel
def set_dds_phase_mu(self, profile, pow_):
"""Set only POW in profile DDS coefficients.

See :meth:`set_dds_mu` for setting the complete DDS profile.

:param profile: Profile number (0-31)
:param pow_: Phase offset word (16 bit)
"""
base = self.servo.coeff_sel | (self.servo_channel <<
(3 + PROFILE_WIDTH)) | (profile << 3)
self.servo.write(base + 2, pow_)

@kernel
def set_dds_phase(self, profile, phase):
"""Set only phase in profile DDS coefficients.

See :meth:`set_dds` for setting the complete DDS profile.

:param profile: Profile number (0-31)
:param phase: DDS phase in turns
"""
self.set_dds_phase_mu(profile, self.dds.turns_to_pow(phase))

@kernel
def set_iir_mu(self, profile, adc, a1, b0, b1, dly=0):
"""Set profile IIR coefficients in machine units.
Expand Down Expand Up @@ -378,7 +413,8 @@ def set_iir_mu(self, profile, adc, a1, b0, b1, dly=0):
:param dly: IIR update suppression time. In units of IIR cycles
(~1.2 µs, 0-255).
"""
base = (self.servo_channel << 8) | (profile << 3)
base = self.servo.coeff_sel | (self.servo_channel <<
(3 + PROFILE_WIDTH)) | (profile << 3)
self.servo.write(base + 3, adc | (dly << 8))
self.servo.write(base + 1, b1)
self.servo.write(base + 5, a1)
Expand Down Expand Up @@ -470,7 +506,9 @@ def get_profile_mu(self, profile, data):
:param profile: Profile number (0-31)
:param data: List of 8 integers to write the profile data into
"""
base = (self.servo_channel << 8) | (profile << 3)
assert len(data) == 8
base = self.servo.coeff_sel | (self.servo_channel <<
(3 + PROFILE_WIDTH)) | (profile << 3)
for i in range(len(data)):
data[i] = self.servo.read(base + i)
delay(4*us)
Expand All @@ -491,7 +529,8 @@ def get_y_mu(self, profile):
:param profile: Profile number (0-31)
:return: 17 bit unsigned Y0
"""
return self.servo.read(STATE_SEL | (self.servo_channel << 5) | profile)
return self.servo.read(self.servo.state_sel | (
self.servo_channel << PROFILE_WIDTH) | profile)

@kernel
def get_y(self, profile):
Expand Down Expand Up @@ -529,7 +568,8 @@ def set_y_mu(self, profile, y):
"""
# State memory is 25 bits wide and signed.
# Reads interact with the 18 MSBs (coefficient memory width)
self.servo.write(STATE_SEL | (self.servo_channel << 5) | profile, y)
self.servo.write(self.servo.state_sel | (
self.servo_channel << PROFILE_WIDTH) | profile, y)

@kernel
def set_y(self, profile, y):
Expand Down
38 changes: 16 additions & 22 deletions artiq/gateware/eem.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,11 +473,10 @@ def add_std(cls, target, eem, eem_aux=None, eem_aux2=None, ttl_out_cls=None,
class SUServo(_EEM):
@staticmethod
def io(*eems, iostandard):
assert len(eems) in (4, 6)
io = (Sampler.io(*eems[0:2], iostandard=iostandard)
+ Urukul.io_qspi(*eems[2:4], iostandard=iostandard))
if len(eems) == 6: # two Urukuls
io += Urukul.io_qspi(*eems[4:6], iostandard=iostandard)
assert len(eems) >= 4 and len(eems) % 2 == 0
io = Sampler.io(*eems[0:2], iostandard=iostandard)
for i in range(len(eems) // 2 - 1):
io += Urukul.io_qspi(*eems[(2 * i + 2):(2 * i + 4)], iostandard=iostandard)
return io

@classmethod
Expand Down Expand Up @@ -516,10 +515,9 @@ def add_std(cls, target, eems_sampler, eems_urukul,
# difference (4 cycles measured)
t_conv=57 - 4, t_rtt=t_rtt + 4)
iir_p = servo.IIRWidths(state=25, coeff=18, adc=16, asf=14, word=16,
accu=48, shift=shift, channel=3,
profile=profile, dly=8)
accu=48, shift=shift, profile=profile, dly=8)
dds_p = servo.DDSParams(width=8 + 32 + 16 + 16,
channels=adc_p.channels, clk=clk)
channels=4 * len(eem_urukul), clk=clk)
su = servo.Servo(sampler_pads, urukul_pads, adc_p, iir_p, dds_p)
su = ClockDomainsRenamer("rio_phy")(su)
# explicitly name the servo submodule to enable the migen namer to derive
Expand All @@ -540,27 +538,23 @@ def add_std(cls, target, eems_sampler, eems_urukul,
target.submodules += phy
target.rtio_channels.append(rtio.Channel.from_phy(phy, ififo_depth=4))

for i in range(2):
if len(eem_urukul) > i:
spi_p, spi_n = (
target.platform.request("{}_spi_p".format(eem_urukul[i])),
target.platform.request("{}_spi_n".format(eem_urukul[i])))
else: # create a dummy bus
spi_p = Record([("clk", 1), ("cs_n", 1)]) # mosi, cs_n
spi_n = None

dds_sync = Signal(reset=0)
for j, eem_urukuli in enumerate(eem_urukul):
# connect quad-SPI
spi_p, spi_n = (
target.platform.request("{}_spi_p".format(eem_urukuli)),
target.platform.request("{}_spi_n".format(eem_urukuli)))
phy = spi2.SPIMaster(spi_p, spi_n)
target.submodules += phy
target.rtio_channels.append(rtio.Channel.from_phy(phy, ififo_depth=4))

for j, eem_urukuli in enumerate(eem_urukul):
# connect `reset_sync_in`
pads = target.platform.request("{}_dds_reset_sync_in".format(eem_urukuli))
target.specials += DifferentialOutput(0, pads.p, pads.n)

target.specials += DifferentialOutput(dds_sync, pads.p, pads.n)
# connect RF switches
for i, signal in enumerate("sw0 sw1 sw2 sw3".split()):
pads = target.platform.request("{}_{}".format(eem_urukuli, signal))
target.specials += DifferentialOutput(
su.iir.ctrl[j*4 + i].en_out, pads.p, pads.n)
su.iir.ctrl[j * 4 + i].en_out, pads.p, pads.n)


class Mirny(_EEM):
Expand Down
Loading