suservo: port to NAC3

This commit is contained in:
Sebastien Bourdeauducq 2022-03-02 08:55:27 +08:00
parent be07481eb5
commit 8fc0e5d3aa
1 changed files with 71 additions and 46 deletions

View File

@ -1,8 +1,19 @@
from artiq.language.core import kernel, delay, delay_mu, portable from numpy import int32, int64
from artiq.language.core import *
from artiq.language.units import us, ns from artiq.language.units import us, ns
from artiq.coredevice.core import Core
from artiq.coredevice.rtio import rtio_output, rtio_input_data from artiq.coredevice.rtio import rtio_output, rtio_input_data
from artiq.coredevice import spi2 as spi from artiq.coredevice.spi2 import SPI_END, SPIMaster
from artiq.coredevice import urukul, sampler from artiq.coredevice.urukul import CFG_MASK_NU, CPLD
from artiq.coredevice.ad9910 import AD9910
from artiq.coredevice.sampler import adc_mu_to_volt as sampler_adc_mu_to_volt, SPI_CONFIG as SAMPLER_SPI_CONFIG, SPI_CS_PGIA as SAMPLER_SPI_CS_PGIA
# NAC3TODO work around https://git.m-labs.hk/M-Labs/nac3/issues/189
@nac3
class ValueError(Exception):
pass
COEFF_WIDTH = 18 COEFF_WIDTH = 18
@ -17,20 +28,21 @@ COEFF_SHIFT = 11
@portable @portable
def y_mu_to_full_scale(y): def y_mu_to_full_scale(y: int32) -> float:
"""Convert servo Y data from machine units to units of full scale.""" """Convert servo Y data from machine units to units of full scale."""
return y / Y_FULL_SCALE_MU return float(y) / float(Y_FULL_SCALE_MU)
@portable @portable
def adc_mu_to_volts(x, gain): def adc_mu_to_volts(x: int32, gain: int32) -> float:
"""Convert servo ADC data from machine units to Volt.""" """Convert servo ADC data from machine units to Volt."""
val = (x >> 1) & 0xffff val = (x >> 1) & 0xffff
mask = 1 << 15 mask = 1 << 15
val = -(val & mask) + (val & ~mask) val = -(val & mask) + (val & ~mask)
return sampler.adc_mu_to_volt(val, gain) return sampler_adc_mu_to_volt(val, gain)
@nac3
class SUServo: class SUServo:
"""Sampler-Urukul Servo parent and configuration device. """Sampler-Urukul Servo parent and configuration device.
@ -64,8 +76,15 @@ class SUServo:
between experiments. between experiments.
:param core_device: Core device name :param core_device: Core device name
""" """
kernel_invariants = {"channel", "core", "pgia", "cplds", "ddses",
"ref_period_mu"} core: KernelInvariant[Core]
pgia: KernelInvariant[SPIMaster]
ddses: KernelInvariant[list[AD9910]]
cplds: KernelInvariant[list[CPLD]]
channel: KernelInvariant[int32]
gains: Kernel[int32]
ref_period_mu: KernelInvariant[int64]
def __init__(self, dmgr, channel, pgia_device, def __init__(self, dmgr, channel, pgia_device,
cpld_devices, dds_devices, cpld_devices, dds_devices,
@ -97,11 +116,11 @@ class SUServo:
or the channel controls. or the channel controls.
""" """
self.set_config(enable=0) self.set_config(enable=0)
delay(3*us) # pipeline flush self.core.delay(3.*us) # pipeline flush
self.pgia.set_config_mu( self.pgia.set_config_mu(
sampler.SPI_CONFIG | spi.SPI_END, SAMPLER_SPI_CONFIG | SPI_END,
16, 4, sampler.SPI_CS_PGIA) 16, 4, SAMPLER_SPI_CS_PGIA)
for i in range(len(self.cplds)): for i in range(len(self.cplds)):
cpld = self.cplds[i] cpld = self.cplds[i]
@ -109,12 +128,12 @@ class SUServo:
cpld.init(blind=True) cpld.init(blind=True)
prev_cpld_cfg = cpld.cfg_reg prev_cpld_cfg = cpld.cfg_reg
cpld.cfg_write(prev_cpld_cfg | (0xf << urukul.CFG_MASK_NU)) cpld.cfg_write(prev_cpld_cfg | (0xf << CFG_MASK_NU))
dds.init(blind=True) dds.init(blind=True)
cpld.cfg_write(prev_cpld_cfg) cpld.cfg_write(prev_cpld_cfg)
@kernel @kernel
def write(self, addr, value): def write(self, addr: int32, value: int32):
"""Write to servo memory. """Write to servo memory.
This method advances the timeline by one coarse RTIO cycle. This method advances the timeline by one coarse RTIO cycle.
@ -130,7 +149,7 @@ class SUServo:
delay_mu(self.ref_period_mu) delay_mu(self.ref_period_mu)
@kernel @kernel
def read(self, addr): def read(self, addr: int32) -> int32:
"""Read from servo memory. """Read from servo memory.
This method does not advance the timeline but consumes all slack. This method does not advance the timeline but consumes all slack.
@ -143,7 +162,7 @@ class SUServo:
return rtio_input_data(self.channel) return rtio_input_data(self.channel)
@kernel @kernel
def set_config(self, enable): def set_config(self, enable: int32):
"""Set SU Servo configuration. """Set SU Servo configuration.
This method advances the timeline by one servo memory access. This method advances the timeline by one servo memory access.
@ -161,7 +180,7 @@ class SUServo:
self.write(CONFIG_ADDR, enable) self.write(CONFIG_ADDR, enable)
@kernel @kernel
def get_status(self): def get_status(self) -> int32:
"""Get current SU Servo status. """Get current SU Servo status.
This method does not advance the timeline but consumes all slack. This method does not advance the timeline but consumes all slack.
@ -182,7 +201,7 @@ class SUServo:
return self.read(CONFIG_ADDR) return self.read(CONFIG_ADDR)
@kernel @kernel
def get_adc_mu(self, adc): def get_adc_mu(self, adc: int32) -> int32:
"""Get the latest ADC reading (IIR filter input X0) in machine units. """Get the latest ADC reading (IIR filter input X0) in machine units.
This method does not advance the timeline but consumes all slack. This method does not advance the timeline but consumes all slack.
@ -200,7 +219,7 @@ class SUServo:
return self.read(STATE_SEL | (adc << 1) | (1 << 8)) return self.read(STATE_SEL | (adc << 1) | (1 << 8))
@kernel @kernel
def set_pgia_mu(self, channel, gain): def set_pgia_mu(self, channel: int32, gain: int32):
"""Set instrumentation amplifier gain of a ADC channel. """Set instrumentation amplifier gain of a ADC channel.
The four gain settings (0, 1, 2, 3) corresponds to gains of The four gain settings (0, 1, 2, 3) corresponds to gains of
@ -216,7 +235,7 @@ class SUServo:
self.gains = gains self.gains = gains
@kernel @kernel
def get_adc(self, channel): def get_adc(self, channel: int32) -> float:
"""Get the latest ADC reading (IIR filter input X0). """Get the latest ADC reading (IIR filter input X0).
This method does not advance the timeline but consumes all slack. This method does not advance the timeline but consumes all slack.
@ -237,13 +256,19 @@ class SUServo:
return adc_mu_to_volts(val, gain) return adc_mu_to_volts(val, gain)
@nac3
class Channel: class Channel:
"""Sampler-Urukul Servo channel """Sampler-Urukul Servo channel
:param channel: RTIO channel number :param channel: RTIO channel number
:param servo_device: Name of the parent SUServo device :param servo_device: Name of the parent SUServo device
""" """
kernel_invariants = {"channel", "core", "servo", "servo_channel"}
core: KernelInvariant[Core]
servo: KernelInvariant[SUServo]
channel: KernelInvariant[int32]
servo_channel: KernelInvariant[int32]
dds: KernelInvariant[AD9910]
def __init__(self, dmgr, channel, servo_device): def __init__(self, dmgr, channel, servo_device):
self.servo = dmgr.get(servo_device) self.servo = dmgr.get(servo_device)
@ -256,7 +281,7 @@ class Channel:
self.dds = self.servo.ddses[self.servo_channel // 4] self.dds = self.servo.ddses[self.servo_channel // 4]
@kernel @kernel
def set(self, en_out, en_iir=0, profile=0): def set(self, en_out: bool, en_iir: bool = False, profile: int32 = 0):
"""Operate channel. """Operate channel.
This method does not advance the timeline. Output RF switch setting This method does not advance the timeline. Output RF switch setting
@ -272,10 +297,10 @@ class Channel:
:param profile: Active profile (0-31) :param profile: Active profile (0-31)
""" """
rtio_output(self.channel << 8, rtio_output(self.channel << 8,
en_out | (en_iir << 1) | (profile << 2)) int32(en_out) | (int32(en_iir) << 1) | (profile << 2))
@kernel @kernel
def set_dds_mu(self, profile, ftw, offs, pow_=0): def set_dds_mu(self, profile: int32, ftw: int32, offs: int32, pow_: int32 = 0):
"""Set profile DDS coefficients in machine units. """Set profile DDS coefficients in machine units.
.. seealso:: :meth:`set_amplitude` .. seealso:: :meth:`set_amplitude`
@ -292,7 +317,7 @@ class Channel:
self.servo.write(base + 2, pow_) self.servo.write(base + 2, pow_)
@kernel @kernel
def set_dds(self, profile, frequency, offset, phase=0.): def set_dds(self, profile: int32, frequency: float, offset: float, phase: float = 0.):
"""Set profile DDS coefficients. """Set profile DDS coefficients.
This method advances the timeline by four servo memory accesses. This method advances the timeline by four servo memory accesses.
@ -311,7 +336,7 @@ class Channel:
self.set_dds_mu(profile, ftw, offs, pow_) self.set_dds_mu(profile, ftw, offs, pow_)
@kernel @kernel
def set_dds_offset_mu(self, profile, offs): def set_dds_offset_mu(self, profile: int32, offs: int32):
"""Set only IIR offset in DDS coefficient profile. """Set only IIR offset in DDS coefficient profile.
See :meth:`set_dds_mu` for setting the complete DDS profile. See :meth:`set_dds_mu` for setting the complete DDS profile.
@ -323,7 +348,7 @@ class Channel:
self.servo.write(base + 4, offs) self.servo.write(base + 4, offs)
@kernel @kernel
def set_dds_offset(self, profile, offset): def set_dds_offset(self, profile: int32, offset: float):
"""Set only IIR offset in DDS coefficient profile. """Set only IIR offset in DDS coefficient profile.
See :meth:`set_dds` for setting the complete DDS profile. See :meth:`set_dds` for setting the complete DDS profile.
@ -334,7 +359,7 @@ class Channel:
self.set_dds_offset_mu(profile, self.dds_offset_to_mu(offset)) self.set_dds_offset_mu(profile, self.dds_offset_to_mu(offset))
@portable @portable
def dds_offset_to_mu(self, offset): def dds_offset_to_mu(self, offset: float) -> int32:
"""Convert IIR offset (negative setpoint) from units of full scale to """Convert IIR offset (negative setpoint) from units of full scale to
machine units (see :meth:`set_dds_mu`, :meth:`set_dds_offset_mu`). machine units (see :meth:`set_dds_mu`, :meth:`set_dds_offset_mu`).
@ -342,10 +367,10 @@ class Channel:
rounding and representation as two's complement, ``offset=1`` can not rounding and representation as two's complement, ``offset=1`` can not
be represented while ``offset=-1`` can. be represented while ``offset=-1`` can.
""" """
return int(round(offset * (1 << COEFF_WIDTH - 1))) return round(offset * float(1 << COEFF_WIDTH - 1))
@kernel @kernel
def set_iir_mu(self, profile, adc, a1, b0, b1, dly=0): def set_iir_mu(self, profile: int32, adc: int32, a1: int32, b0: int32, b1: int32, dly: int32 = 0):
"""Set profile IIR coefficients in machine units. """Set profile IIR coefficients in machine units.
The recurrence relation is (all data signed and MSB aligned): The recurrence relation is (all data signed and MSB aligned):
@ -385,7 +410,7 @@ class Channel:
self.servo.write(base + 7, b0) self.servo.write(base + 7, b0)
@kernel @kernel
def set_iir(self, profile, adc, kp, ki=0., g=0., delay=0.): def set_iir(self, profile: int32, adc: int32, kp: float, ki: float = 0., g: float = 0., delay: float = 0.):
"""Set profile IIR coefficients. """Set profile IIR coefficients.
This method advances the timeline by four servo memory accesses. This method advances the timeline by four servo memory accesses.
@ -427,23 +452,23 @@ class Channel:
A_NORM = 1 << COEFF_SHIFT A_NORM = 1 << COEFF_SHIFT
COEFF_MAX = 1 << COEFF_WIDTH - 1 COEFF_MAX = 1 << COEFF_WIDTH - 1
kp *= B_NORM kp *= float(B_NORM)
if ki == 0.: if ki == 0.:
# pure P # pure P
a1 = 0 a1 = 0
b1 = 0 b1 = 0
b0 = int(round(kp)) b0 = round(kp)
else: else:
# I or PI # I or PI
ki *= B_NORM*T_CYCLE/2. ki *= float(B_NORM)*T_CYCLE/2.
if g == 0.: if g == 0.:
c = 1. c = 1.
a1 = A_NORM a1 = A_NORM
else: else:
c = 1./(1. + ki/(g*B_NORM)) c = 1./(1. + ki/(g*float(B_NORM)))
a1 = int(round((2.*c - 1.)*A_NORM)) a1 = round((2.*c - 1.)*float(A_NORM))
b0 = int(round(kp + ki*c)) b0 = round(kp + ki*c)
b1 = int(round(kp + (ki - 2.*kp)*c)) b1 = round(kp + (ki - 2.*kp)*c)
if b1 == -b0: if b1 == -b0:
raise ValueError("low integrator gain and/or gain limit") raise ValueError("low integrator gain and/or gain limit")
@ -451,11 +476,11 @@ class Channel:
b1 >= COEFF_MAX or b1 < -COEFF_MAX): b1 >= COEFF_MAX or b1 < -COEFF_MAX):
raise ValueError("high gains") raise ValueError("high gains")
dly = int(round(delay/T_CYCLE)) dly = round(delay/T_CYCLE)
self.set_iir_mu(profile, adc, a1, b0, b1, dly) self.set_iir_mu(profile, adc, a1, b0, b1, dly)
@kernel @kernel
def get_profile_mu(self, profile, data): def get_profile_mu(self, profile: int32, data: list[int32]):
"""Retrieve profile data. """Retrieve profile data.
Profile data is returned in the ``data`` argument in machine units Profile data is returned in the ``data`` argument in machine units
@ -473,10 +498,10 @@ class Channel:
base = (self.servo_channel << 8) | (profile << 3) base = (self.servo_channel << 8) | (profile << 3)
for i in range(len(data)): for i in range(len(data)):
data[i] = self.servo.read(base + i) data[i] = self.servo.read(base + i)
delay(4*us) self.core.delay(4.*us)
@kernel @kernel
def get_y_mu(self, profile): def get_y_mu(self, profile: int32) -> int32:
"""Get a profile's IIR state (filter output, Y0) in machine units. """Get a profile's IIR state (filter output, Y0) in machine units.
The IIR state is also know as the "integrator", or the DDS amplitude The IIR state is also know as the "integrator", or the DDS amplitude
@ -494,7 +519,7 @@ class Channel:
return self.servo.read(STATE_SEL | (self.servo_channel << 5) | profile) return self.servo.read(STATE_SEL | (self.servo_channel << 5) | profile)
@kernel @kernel
def get_y(self, profile): def get_y(self, profile: int32) -> float:
"""Get a profile's IIR state (filter output, Y0). """Get a profile's IIR state (filter output, Y0).
The IIR state is also know as the "integrator", or the DDS amplitude The IIR state is also know as the "integrator", or the DDS amplitude
@ -512,7 +537,7 @@ class Channel:
return y_mu_to_full_scale(self.get_y_mu(profile)) return y_mu_to_full_scale(self.get_y_mu(profile))
@kernel @kernel
def set_y_mu(self, profile, y): def set_y_mu(self, profile: int32, y: int32):
"""Set a profile's IIR state (filter output, Y0) in machine units. """Set a profile's IIR state (filter output, Y0) in machine units.
The IIR state is also know as the "integrator", or the DDS amplitude The IIR state is also know as the "integrator", or the DDS amplitude
@ -532,7 +557,7 @@ class Channel:
self.servo.write(STATE_SEL | (self.servo_channel << 5) | profile, y) self.servo.write(STATE_SEL | (self.servo_channel << 5) | profile, y)
@kernel @kernel
def set_y(self, profile, y): def set_y(self, profile: int32, y: float) -> int32:
"""Set a profile's IIR state (filter output, Y0). """Set a profile's IIR state (filter output, Y0).
The IIR state is also know as the "integrator", or the DDS amplitude The IIR state is also know as the "integrator", or the DDS amplitude
@ -547,7 +572,7 @@ class Channel:
:param profile: Profile number (0-31) :param profile: Profile number (0-31)
:param y: IIR state in units of full scale :param y: IIR state in units of full scale
""" """
y_mu = int(round(y * Y_FULL_SCALE_MU)) y_mu = round(y * float(Y_FULL_SCALE_MU))
if y_mu < 0 or y_mu > (1 << 17) - 1: if y_mu < 0 or y_mu > (1 << 17) - 1:
raise ValueError("Invalid SUServo y-value!") raise ValueError("Invalid SUServo y-value!")
self.set_y_mu(profile, y_mu) self.set_y_mu(profile, y_mu)