forked from M-Labs/artiq
2366 lines
96 KiB
Diff
2366 lines
96 KiB
Diff
diff --git a/artiq/coredevice/ad9910.py b/artiq/coredevice/ad9910.py
|
||
index 801b689ca0..bc19afe25c 100644
|
||
--- a/artiq/coredevice/ad9910.py
|
||
+++ b/artiq/coredevice/ad9910.py
|
||
@@ -277,6 +277,10 @@ def read32(self, addr: TInt32) -> TInt32:
|
||
|
||
:param addr: Register address
|
||
"""
|
||
+ return self.read32_impl(addr)
|
||
+
|
||
+ @kernel
|
||
+ def read32_impl(self, addr):
|
||
self.bus.set_config_mu(urukul.SPI_CONFIG, 8,
|
||
urukul.SPIT_DDS_WR, self.chip_select)
|
||
self.bus.write((addr | 0x80) << 24)
|
||
@@ -981,7 +985,8 @@ def clear_smp_err(self):
|
||
|
||
@kernel
|
||
def tune_sync_delay(self,
|
||
- search_seed: TInt32 = 15) -> TTuple([TInt32, TInt32]):
|
||
+ search_seed: TInt32 = 15,
|
||
+ cpld_channel_idx: TInt32 = -1) -> TTuple([TInt32, TInt32]):
|
||
"""Find a stable SYNC_IN delay.
|
||
|
||
This method first locates a valid SYNC_IN delay at zero validation
|
||
@@ -997,6 +1002,9 @@ def tune_sync_delay(self,
|
||
Defaults to 15 (half range).
|
||
:return: Tuple of optimal delay and window size.
|
||
"""
|
||
+ if cpld_channel_idx == -1:
|
||
+ cpld_channel_idx = self.chip_select - 4
|
||
+ assert 0 <= cpld_channel_idx < 4, "Invalid channel index"
|
||
if not self.cpld.sync_div:
|
||
raise ValueError("parent cpld does not drive SYNC")
|
||
search_span = 31
|
||
@@ -1019,7 +1027,7 @@ def tune_sync_delay(self,
|
||
delay(100 * us)
|
||
err = urukul_sta_smp_err(self.cpld.sta_read())
|
||
delay(100 * us) # slack
|
||
- if not (err >> (self.chip_select - 4)) & 1:
|
||
+ if not (err >> cpld_channel_idx) & 1:
|
||
next_seed = in_delay
|
||
break
|
||
if next_seed >= 0: # valid delay found, scan next window
|
||
diff --git a/artiq/coredevice/suservo.py b/artiq/coredevice/suservo.py
|
||
index 1d0a72dad1..f7b516a4e7 100644
|
||
--- a/artiq/coredevice/suservo.py
|
||
+++ b/artiq/coredevice/suservo.py
|
||
@@ -1,19 +1,19 @@
|
||
from artiq.language.core import kernel, delay, delay_mu, portable
|
||
from artiq.language.units import us, ns
|
||
+from artiq.language import *
|
||
from artiq.coredevice.rtio import rtio_output, rtio_input_data
|
||
from artiq.coredevice import spi2 as spi
|
||
-from artiq.coredevice import urukul, sampler
|
||
+from artiq.coredevice import urukul, sampler, ad9910
|
||
+from math import ceil, log2
|
||
+from numpy import int32, int64
|
||
|
||
|
||
-COEFF_WIDTH = 18
|
||
+COEFF_WIDTH = 18 # Must match gateware IIRWidths.coeff
|
||
Y_FULL_SCALE_MU = (1 << (COEFF_WIDTH - 1)) - 1
|
||
-COEFF_DEPTH = 10 + 1
|
||
-WE = 1 << COEFF_DEPTH + 1
|
||
-STATE_SEL = 1 << COEFF_DEPTH
|
||
-CONFIG_SEL = 1 << COEFF_DEPTH - 1
|
||
-CONFIG_ADDR = CONFIG_SEL | STATE_SEL
|
||
T_CYCLE = (2*(8 + 64) + 2)*8*ns # Must match gateware Servo.t_cycle.
|
||
-COEFF_SHIFT = 11
|
||
+COEFF_SHIFT = 11 # Must match gateware IIRWidths.shift
|
||
+PROFILE_WIDTH = 5 # Must match gateware IIRWidths.profile
|
||
+FINE_TS_WIDTH = 3 # Must match gateware IIRWidths.ioup_dly
|
||
|
||
|
||
@portable
|
||
@@ -35,21 +35,21 @@ class SUServo:
|
||
"""Sampler-Urukul Servo parent and configuration device.
|
||
|
||
Sampler-Urukul Servo is a integrated device controlling one
|
||
- 8-channel ADC (Sampler) and two 4-channel DDS (Urukuls) with a DSP engine
|
||
- connecting the ADC data and the DDS output amplitudes to enable
|
||
+ 8-channel ADC (Sampler) and any number of 4-channel DDS (Urukuls) with a
|
||
+ DSP engine connecting the ADC data and the DDS output amplitudes to enable
|
||
feedback. SU Servo can for example be used to implement intensity
|
||
stabilization of laser beams with an amplifier and AOM driven by Urukul
|
||
and a photodetector connected to Sampler.
|
||
|
||
Additionally SU Servo supports multiple preconfigured profiles per channel
|
||
- and features like automatic integrator hold.
|
||
+ and features like automatic integrator hold and coherent phase tracking.
|
||
|
||
Notes:
|
||
|
||
* See the SU Servo variant of the Kasli target for an example of how to
|
||
connect the gateware and the devices. Sampler and each Urukul need
|
||
two EEM connections.
|
||
- * Ensure that both Urukuls are AD9910 variants and have the on-board
|
||
+ * Ensure that all Urukuls are AD9910 variants and have the on-board
|
||
dip switches set to 1100 (first two on, last two off).
|
||
* Refer to the Sampler and Urukul documentation and the SU Servo
|
||
example device database for runtime configuration of the devices
|
||
@@ -65,7 +65,9 @@ class SUServo:
|
||
:param core_device: Core device name
|
||
"""
|
||
kernel_invariants = {"channel", "core", "pgia", "cplds", "ddses",
|
||
- "ref_period_mu", "corrected_fs"}
|
||
+ "ref_period_mu", "num_channels", "coeff_sel",
|
||
+ "state_sel", "io_dly_addr", "config_addr",
|
||
+ "corrected_fs", "write_enable"}
|
||
|
||
def __init__(self, dmgr, channel, pgia_device,
|
||
cpld_devices, dds_devices,
|
||
@@ -83,13 +85,24 @@ def __init__(self, dmgr, channel, pgia_device,
|
||
self.corrected_fs = sampler.Sampler.use_corrected_fs(sampler_hw_rev)
|
||
assert self.ref_period_mu == self.core.ref_multiplier
|
||
|
||
+ # The width of parts of the servo memory address depends on the number
|
||
+ # of channels.
|
||
+ self.num_channels = 4 * len(dds_devices)
|
||
+ channel_width = ceil(log2(self.num_channels))
|
||
+ coeff_depth = PROFILE_WIDTH + channel_width + 3
|
||
+ self.io_dly_addr = 1 << (coeff_depth - 2)
|
||
+ self.state_sel = 2 << (coeff_depth - 2)
|
||
+ self.config_addr = 3 << (coeff_depth - 2)
|
||
+ self.coeff_sel = 1 << coeff_depth
|
||
+ self.write_enable = 1 << (coeff_depth + 1)
|
||
+
|
||
@staticmethod
|
||
def get_rtio_channels(channel, **kwargs):
|
||
return [(channel, None)]
|
||
|
||
@kernel
|
||
def init(self):
|
||
- """Initialize the servo, Sampler and both Urukuls.
|
||
+ """Initialize the servo, Sampler and all Urukuls.
|
||
|
||
Leaves the servo disabled (see :meth:`set_config`), resets and
|
||
configures all DDS.
|
||
@@ -111,8 +124,20 @@ def init(self):
|
||
prev_cpld_cfg = cpld.cfg_reg
|
||
cpld.cfg_write(prev_cpld_cfg | (0xf << urukul.CFG_MASK_NU))
|
||
dds.init(blind=True)
|
||
+
|
||
+ if dds.sync_data.sync_delay_seed != -1:
|
||
+ for channel_idx in range(4):
|
||
+ mask_nu_this = 1 << (urukul.CFG_MASK_NU + channel_idx)
|
||
+ cpld.cfg_write(prev_cpld_cfg | mask_nu_this)
|
||
+ delay(8 * us)
|
||
+ dds.tune_sync_delay(dds.sync_data.sync_delay_seed,
|
||
+ cpld_channel_idx=channel_idx)
|
||
+ delay(50 * us)
|
||
cpld.cfg_write(prev_cpld_cfg)
|
||
|
||
+ self.set_io_update_delays(
|
||
+ [dds.sync_data.io_update_delay for dds in self.ddses])
|
||
+
|
||
@kernel
|
||
def write(self, addr, value):
|
||
"""Write to servo memory.
|
||
@@ -122,7 +147,7 @@ def write(self, addr, value):
|
||
:param addr: Memory location address.
|
||
:param value: Data to be written.
|
||
"""
|
||
- addr |= WE
|
||
+ addr |= self.write_enable
|
||
value &= (1 << COEFF_WIDTH) - 1
|
||
value |= (addr >> 8) << COEFF_WIDTH
|
||
addr = addr & 0xff
|
||
@@ -158,7 +183,7 @@ def set_config(self, enable):
|
||
Disabling takes up to two servo cycles (~2.3 µs) to clear the
|
||
processing pipeline.
|
||
"""
|
||
- self.write(CONFIG_ADDR, enable)
|
||
+ self.write(self.config_addr, enable)
|
||
|
||
@kernel
|
||
def get_status(self):
|
||
@@ -179,7 +204,7 @@ def get_status(self):
|
||
:return: Status. Bit 0: enabled, bit 1: done,
|
||
bits 8-15: channel clip indicators.
|
||
"""
|
||
- return self.read(CONFIG_ADDR)
|
||
+ return self.read(self.config_addr)
|
||
|
||
@kernel
|
||
def get_adc_mu(self, adc):
|
||
@@ -197,7 +222,8 @@ def get_adc_mu(self, adc):
|
||
# State memory entries are 25 bits. Due to the pre-adder dynamic
|
||
# range, X0/X1/OFFSET are only 24 bits. Finally, the RTIO interface
|
||
# only returns the 18 MSBs (the width of the coefficient memory).
|
||
- return self.read(STATE_SEL | (adc << 1) | (1 << 8))
|
||
+ return self.read(self.state_sel |
|
||
+ (2 * adc + (1 << PROFILE_WIDTH) * self.num_channels))
|
||
|
||
@kernel
|
||
def set_pgia_mu(self, channel, gain):
|
||
@@ -242,6 +268,18 @@ def get_adc(self, channel):
|
||
gain = (self.gains >> (channel*2)) & 0b11
|
||
return adc_mu_to_volts(val, gain, self.corrected_fs)
|
||
|
||
+ @kernel
|
||
+ def set_io_update_delays(self, dlys):
|
||
+ """Set IO_UPDATE pulse alignment delays.
|
||
+
|
||
+ :param dlys: List of delays for each Urukul
|
||
+ """
|
||
+ bits = 0
|
||
+ mask_fine_ts = (1 << FINE_TS_WIDTH) - 1
|
||
+ for i in range(len(dlys)):
|
||
+ bits |= (dlys[i] & mask_fine_ts) << (FINE_TS_WIDTH * i)
|
||
+ self.write(self.io_dly_addr, bits)
|
||
+
|
||
|
||
class Channel:
|
||
"""Sampler-Urukul Servo channel
|
||
@@ -256,7 +294,7 @@ def __init__(self, dmgr, channel, servo_device):
|
||
return [(channel, None)]
|
||
|
||
@kernel
|
||
- def set(self, en_out, en_iir=0, profile=0):
|
||
+ def set(self, en_out, en_iir=0, profile=0, en_pt=0):
|
||
"""Operate channel.
|
||
|
||
This method does not advance the timeline. Output RF switch setting
|
||
@@ -270,9 +308,26 @@ def set(self, en_out, en_iir=0, profile=0):
|
||
:param en_out: RF switch enable
|
||
:param en_iir: IIR updates enable
|
||
:param profile: Active profile (0-31)
|
||
+ :param en_pt: Coherent phase tracking enable
|
||
+ * en_pt=1: "coherent phase mode"
|
||
+ * en_pt=0: "continuous phase mode"
|
||
+ (see :func:`artiq.coredevice.ad9910.AD9910.set_phase_mode` for a
|
||
+ definition of the phase modes)
|
||
"""
|
||
rtio_output(self.channel << 8,
|
||
- en_out | (en_iir << 1) | (profile << 2))
|
||
+ en_out | (en_iir << 1) | (en_pt << 2) | (profile << 3))
|
||
+
|
||
+ @kernel
|
||
+ def set_reference_time(self):
|
||
+ """Set reference time for "coherent phase mode" (see :meth:`set`).
|
||
+
|
||
+ This method does not advance the timeline.
|
||
+ With en_pt=1 (see :meth:`set`), the tracked DDS output phase of
|
||
+ this channel will refer to the current timeline position.
|
||
+
|
||
+ """
|
||
+ fine_ts = now_mu() & ((1 << FINE_TS_WIDTH) - 1)
|
||
+ rtio_output(self.channel << 8 | 1, self.dds.sysclk_per_mu * fine_ts)
|
||
|
||
@kernel
|
||
def set_dds_mu(self, profile, ftw, offs, pow_=0):
|
||
@@ -285,10 +340,11 @@ def set_dds_mu(self, profile, ftw, offs, pow_=0):
|
||
:param offs: IIR offset (17 bit signed)
|
||
:param pow_: Phase offset word (16 bit)
|
||
"""
|
||
- base = (self.servo_channel << 8) | (profile << 3)
|
||
+ base = self.servo.coeff_sel | (self.servo_channel <<
|
||
+ (3 + PROFILE_WIDTH)) | (profile << 3)
|
||
self.servo.write(base + 0, ftw >> 16)
|
||
self.servo.write(base + 6, (ftw & 0xffff))
|
||
- self.set_dds_offset_mu(profile, offs)
|
||
+ self.servo.write(base + 4, offs)
|
||
self.servo.write(base + 2, pow_)
|
||
|
||
@kernel
|
||
@@ -319,7 +375,8 @@ def set_dds_offset_mu(self, profile, offs):
|
||
:param profile: Profile number (0-31)
|
||
:param offs: IIR offset (17 bit signed)
|
||
"""
|
||
- base = (self.servo_channel << 8) | (profile << 3)
|
||
+ base = self.servo.coeff_sel | (self.servo_channel <<
|
||
+ (3 + PROFILE_WIDTH)) | (profile << 3)
|
||
self.servo.write(base + 4, offs)
|
||
|
||
@kernel
|
||
@@ -344,6 +401,30 @@ def dds_offset_to_mu(self, offset):
|
||
"""
|
||
return int(round(offset * (1 << COEFF_WIDTH - 1)))
|
||
|
||
+ @kernel
|
||
+ def set_dds_phase_mu(self, profile, pow_):
|
||
+ """Set only POW in profile DDS coefficients.
|
||
+
|
||
+ See :meth:`set_dds_mu` for setting the complete DDS profile.
|
||
+
|
||
+ :param profile: Profile number (0-31)
|
||
+ :param pow_: Phase offset word (16 bit)
|
||
+ """
|
||
+ base = self.servo.coeff_sel | (self.servo_channel <<
|
||
+ (3 + PROFILE_WIDTH)) | (profile << 3)
|
||
+ self.servo.write(base + 2, pow_)
|
||
+
|
||
+ @kernel
|
||
+ def set_dds_phase(self, profile, phase):
|
||
+ """Set only phase in profile DDS coefficients.
|
||
+
|
||
+ See :meth:`set_dds` for setting the complete DDS profile.
|
||
+
|
||
+ :param profile: Profile number (0-31)
|
||
+ :param phase: DDS phase in turns
|
||
+ """
|
||
+ self.set_dds_phase_mu(profile, self.dds.turns_to_pow(phase))
|
||
+
|
||
@kernel
|
||
def set_iir_mu(self, profile, adc, a1, b0, b1, dly=0):
|
||
"""Set profile IIR coefficients in machine units.
|
||
@@ -378,7 +459,8 @@ def set_iir_mu(self, profile, adc, a1, b0, b1, dly=0):
|
||
:param dly: IIR update suppression time. In units of IIR cycles
|
||
(~1.2 µs, 0-255).
|
||
"""
|
||
- base = (self.servo_channel << 8) | (profile << 3)
|
||
+ base = self.servo.coeff_sel | (self.servo_channel <<
|
||
+ (3 + PROFILE_WIDTH)) | (profile << 3)
|
||
self.servo.write(base + 3, adc | (dly << 8))
|
||
self.servo.write(base + 1, b1)
|
||
self.servo.write(base + 5, a1)
|
||
@@ -470,7 +552,9 @@ def get_profile_mu(self, profile, data):
|
||
:param profile: Profile number (0-31)
|
||
:param data: List of 8 integers to write the profile data into
|
||
"""
|
||
- base = (self.servo_channel << 8) | (profile << 3)
|
||
+ assert len(data) == 8
|
||
+ base = self.servo.coeff_sel | (self.servo_channel <<
|
||
+ (3 + PROFILE_WIDTH)) | (profile << 3)
|
||
for i in range(len(data)):
|
||
data[i] = self.servo.read(base + i)
|
||
delay(4*us)
|
||
@@ -491,7 +575,8 @@ def get_y_mu(self, profile):
|
||
:param profile: Profile number (0-31)
|
||
:return: 17 bit unsigned Y0
|
||
"""
|
||
- return self.servo.read(STATE_SEL | (self.servo_channel << 5) | profile)
|
||
+ return self.servo.read(self.servo.state_sel | (
|
||
+ self.servo_channel << PROFILE_WIDTH) | profile)
|
||
|
||
@kernel
|
||
def get_y(self, profile):
|
||
@@ -529,7 +614,8 @@ def set_y_mu(self, profile, y):
|
||
"""
|
||
# State memory is 25 bits wide and signed.
|
||
# Reads interact with the 18 MSBs (coefficient memory width)
|
||
- self.servo.write(STATE_SEL | (self.servo_channel << 5) | profile, y)
|
||
+ self.servo.write(self.servo.state_sel | (
|
||
+ self.servo_channel << PROFILE_WIDTH) | profile, y)
|
||
|
||
@kernel
|
||
def set_y(self, profile, y):
|
||
@@ -552,3 +638,217 @@ def set_y(self, profile, y):
|
||
raise ValueError("Invalid SUServo y-value!")
|
||
self.set_y_mu(profile, y_mu)
|
||
return y_mu
|
||
+
|
||
+
|
||
+class CPLD(urukul.CPLD):
|
||
+ """
|
||
+ This module contains a subclass of the Urukul driver class in artiq.coredevice
|
||
+ adapted to use CPLD read-back via half-duplex SPI. Only the 8 LSBs can be read
|
||
+ back as the read-back buffer on the CPLD is 8 bits wide.
|
||
+ """
|
||
+
|
||
+ def __init__(self, dmgr, spi_device, io_update_device=None,
|
||
+ **kwargs):
|
||
+ # Separate IO_UPDATE TTL output device used by SUServo core,
|
||
+ # if active, else by artiq.coredevice.suservo.AD9910
|
||
+ # :meth:`measure_io_update_alignment`.
|
||
+ # The urukul.CPLD driver utilises the CPLD CFG register
|
||
+ # option instead for pulsing IO_UPDATE of masked DDSs.
|
||
+ self.io_update_ttl = dmgr.get(io_update_device)
|
||
+ urukul.CPLD.__init__(self, dmgr, spi_device, **kwargs)
|
||
+
|
||
+ @kernel
|
||
+ def enable_readback(self):
|
||
+ """
|
||
+ This method sets the RB_EN flag in the Urukul CPLD configuration
|
||
+ register. Once set, the CPLD expects an alternating sequence of
|
||
+ two SPI transactions:
|
||
+
|
||
+ * 1: Any transaction. If returning data, the 8 LSBs
|
||
+ of that will be stored in the CPLD.
|
||
+
|
||
+ * 2: One read transaction in half-duplex SPI mode shifting
|
||
+ out data from the CPLD over MOSI (use :meth:`readback`).
|
||
+
|
||
+ To end this protocol, call :meth:`disable_readback` during step 1.
|
||
+ """
|
||
+ self.cfg_write(self.cfg_reg | (1 << urukul.CFG_RB_EN))
|
||
+
|
||
+ @kernel
|
||
+ def disable_readback(self):
|
||
+ """
|
||
+ This method clears the RB_EN flag in the Urukul CPLD configuration
|
||
+ register. This marks the end of the readback protocol (see
|
||
+ :meth:`enable_readback`).
|
||
+ """
|
||
+ self.cfg_write(self.cfg_reg & ~(1 << urukul.CFG_RB_EN))
|
||
+
|
||
+ @kernel
|
||
+ def sta_read(self, full=False):
|
||
+ """
|
||
+ Read from status register
|
||
+
|
||
+ :param full: retrieve status register by concatenating data from
|
||
+ several readback transactions.
|
||
+ """
|
||
+ self.enable_readback()
|
||
+ self.sta_read_impl()
|
||
+ delay(16 * us) # slack
|
||
+ r = self.readback() << urukul.STA_RF_SW
|
||
+ delay(16 * us) # slack
|
||
+ if full:
|
||
+ self.enable_readback() # dummy write
|
||
+ r |= self.readback(urukul.CS_RB_PLL_LOCK) << urukul.STA_PLL_LOCK
|
||
+ delay(16 * us) # slack
|
||
+ self.enable_readback() # dummy write
|
||
+ r |= self.readback(urukul.CS_RB_PROTO_REV) << urukul.STA_PROTO_REV
|
||
+ delay(16 * us) # slack
|
||
+ self.disable_readback()
|
||
+ return r
|
||
+
|
||
+ @kernel
|
||
+ def proto_rev_read(self):
|
||
+ """Read 8 LSBs of proto_rev"""
|
||
+ self.enable_readback()
|
||
+ self.enable_readback() # dummy write
|
||
+ r = self.readback(urukul.CS_RB_PROTO_REV)
|
||
+ self.disable_readback()
|
||
+ return r
|
||
+
|
||
+ @kernel
|
||
+ def pll_lock_read(self):
|
||
+ """Read PLL lock status"""
|
||
+ self.enable_readback()
|
||
+ self.enable_readback() # dummy write
|
||
+ r = self.readback(urukul.CS_RB_PLL_LOCK)
|
||
+ self.disable_readback()
|
||
+ return r & 0xf
|
||
+
|
||
+ @kernel
|
||
+ def get_att_mu(self):
|
||
+ # Different behaviour to urukul.CPLD.get_att_mu: Here, the
|
||
+ # latch enable of the attenuators activates 31.5dB
|
||
+ # attenuation during the transactions.
|
||
+ att_reg = int32(0)
|
||
+ self.enable_readback()
|
||
+ for i in range(4):
|
||
+ self.core.break_realtime()
|
||
+ self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 8,
|
||
+ urukul.SPIT_ATT_RD, urukul.CS_ATT)
|
||
+ self.bus.write(0) # shift in zeros, shift out next 8 bits
|
||
+ r = self.readback() & 0xff
|
||
+ att_reg |= r << (8 * i)
|
||
+
|
||
+ delay(16 * us) # slack
|
||
+ self.disable_readback()
|
||
+
|
||
+ self.att_reg = int32(att_reg)
|
||
+ delay(8 * us) # slack
|
||
+ self.set_all_att_mu(self.att_reg) # shift and latch current value again
|
||
+ return self.att_reg
|
||
+
|
||
+ @kernel
|
||
+ def readback(self, cs=urukul.CS_RB_LSBS):
|
||
+ """Read from the readback register in half-duplex SPI mode
|
||
+ See :meth:`enable_readback` for usage instructions.
|
||
+
|
||
+ :param cs: Select data to be returned from the readback register.
|
||
+ - urukul.CS_RB_LSBS does not modify the readback register upon readback
|
||
+ - urukul.CS_RB_PROTO_REV loads the 8 LSBs of proto_rev
|
||
+ - urukul.CS_PLL_LOCK loads the PLL lock status bits concatenated with the
|
||
+ IFC mode bits
|
||
+ :return: CPLD readback register.
|
||
+ """
|
||
+ self.bus.set_config_mu(
|
||
+ urukul.SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT | spi.SPI_HALF_DUPLEX,
|
||
+ 8, urukul.SPIT_CFG_RD, cs)
|
||
+ self.bus.write(0)
|
||
+ return int32(self.bus.read())
|
||
+
|
||
+
|
||
+class AD9910(ad9910.AD9910):
|
||
+ """
|
||
+ This module contains a subclass of the AD9910 driver class in artiq.coredevice
|
||
+ using CPLD read-back via half-duplex SPI.
|
||
+ """
|
||
+
|
||
+ # Re-declare set of kernel invariants to avoid warning about non-existent
|
||
+ # `sw` attribute, as the AD9910 (instance) constructor writes to the
|
||
+ # class attributes.
|
||
+ kernel_invariants = {
|
||
+ "chip_select", "cpld", "core", "bus", "ftw_per_hz", "sysclk_per_mu"
|
||
+ }
|
||
+
|
||
+ @kernel
|
||
+ def read32(self, addr):
|
||
+ """ Read from a 32-bit register
|
||
+
|
||
+ This method returns only the 8 LSBs of the return value.
|
||
+ """
|
||
+ self.cpld.enable_readback()
|
||
+ self.read32_impl(addr)
|
||
+ delay(12 * us) # slack
|
||
+ r = self.cpld.readback()
|
||
+ delay(12 * us) # slack
|
||
+ self.cpld.disable_readback()
|
||
+ return r
|
||
+
|
||
+ @kernel
|
||
+ def read64(self, addr):
|
||
+ # 3-wire SPI transactions consisting of multiple transfers are not supported.
|
||
+ raise NotImplementedError
|
||
+
|
||
+ @kernel
|
||
+ def read_ram(self, data):
|
||
+ # 3-wire SPI transactions consisting of multiple transfers are not supported.
|
||
+ raise NotImplementedError
|
||
+
|
||
+ @kernel
|
||
+ def measure_io_update_alignment(self, delay_start, delay_stop):
|
||
+ """Use the digital ramp generator to locate the alignment between
|
||
+ IO_UPDATE and SYNC_CLK.
|
||
+
|
||
+ Refer to `artiq.coredevice.ad9910` :meth:`measure_io_update_alignment`.
|
||
+ In order that this method can operate the io_update_ttl also used by the SUServo
|
||
+ core, deactivate the servo before (see :meth:`set_config`).
|
||
+ """
|
||
+ # set up DRG
|
||
+ self.set_cfr1(drg_load_lrr=1, drg_autoclear=1)
|
||
+ # DRG -> FTW, DRG enable
|
||
+ self.set_cfr2(drg_enable=1)
|
||
+ # no limits
|
||
+ self.write64(ad9910._AD9910_REG_RAMP_LIMIT, -1, 0)
|
||
+ # DRCTL=0, dt=1 t_SYNC_CLK
|
||
+ self.write32(ad9910._AD9910_REG_RAMP_RATE, 0x00010000)
|
||
+ # dFTW = 1, (work around negative slope)
|
||
+ self.write64(ad9910._AD9910_REG_RAMP_STEP, -1, 0)
|
||
+ # un-mask DDS
|
||
+ cfg_masked = self.cpld.cfg_reg
|
||
+ self.cpld.cfg_write(cfg_masked & ~(0xf << urukul.CFG_MASK_NU))
|
||
+ delay(70 * us) # slack
|
||
+ # delay io_update after RTIO edge
|
||
+ t = now_mu() + 8 & ~7
|
||
+ at_mu(t + delay_start)
|
||
+ # assumes a maximum t_SYNC_CLK period
|
||
+ self.cpld.io_update_ttl.pulse(self.core.mu_to_seconds(16 - delay_start)) # realign
|
||
+ # re-mask DDS
|
||
+ self.cpld.cfg_write(cfg_masked)
|
||
+ delay(10 * us) # slack
|
||
+ # disable DRG autoclear and LRR on io_update
|
||
+ self.set_cfr1()
|
||
+ delay(10 * us) # slack
|
||
+ # stop DRG
|
||
+ self.write64(ad9910._AD9910_REG_RAMP_STEP, 0, 0)
|
||
+ delay(10 * us) # slack
|
||
+ # un-mask DDS
|
||
+ self.cpld.cfg_write(cfg_masked & ~(0xf << urukul.CFG_MASK_NU))
|
||
+ at_mu(t + 0x20000 + delay_stop)
|
||
+ self.cpld.io_update_ttl.pulse_mu(16 - delay_stop) # realign
|
||
+ # re-mask DDS
|
||
+ self.cpld.cfg_write(cfg_masked)
|
||
+ ftw = self.read32(ad9910._AD9910_REG_FTW) # read out effective FTW
|
||
+ delay(100 * us) # slack
|
||
+ # disable DRG
|
||
+ self.set_cfr2(drg_enable=0)
|
||
+ self.cpld.io_update.pulse_mu(16)
|
||
+ return ftw & 1
|
||
diff --git a/artiq/coredevice/urukul.py b/artiq/coredevice/urukul.py
|
||
index 2fd66bd65e..61fd476280 100644
|
||
--- a/artiq/coredevice/urukul.py
|
||
+++ b/artiq/coredevice/urukul.py
|
||
@@ -24,6 +24,7 @@
|
||
CFG_RF_SW = 0
|
||
CFG_LED = 4
|
||
CFG_PROFILE = 8
|
||
+CFG_RB_EN = 11
|
||
CFG_IO_UPDATE = 12
|
||
CFG_MASK_NU = 13
|
||
CFG_CLK_SEL0 = 17
|
||
@@ -51,18 +52,23 @@
|
||
CS_DDS_CH1 = 5
|
||
CS_DDS_CH2 = 6
|
||
CS_DDS_CH3 = 7
|
||
+# chip selects for readback
|
||
+CS_RB_PROTO_REV = 1
|
||
+CS_RB_PLL_LOCK = 2
|
||
+CS_RB_LSBS = 3
|
||
|
||
# Default profile
|
||
DEFAULT_PROFILE = 7
|
||
|
||
|
||
@portable
|
||
-def urukul_cfg(rf_sw, led, profile, io_update, mask_nu,
|
||
+def urukul_cfg(rf_sw, led, profile, rb_en, io_update, mask_nu,
|
||
clk_sel, sync_sel, rst, io_rst, clk_div):
|
||
"""Build Urukul CPLD configuration register"""
|
||
return ((rf_sw << CFG_RF_SW) |
|
||
(led << CFG_LED) |
|
||
(profile << CFG_PROFILE) |
|
||
+ (rb_en << CFG_RB_EN) |
|
||
(io_update << CFG_IO_UPDATE) |
|
||
(mask_nu << CFG_MASK_NU) |
|
||
((clk_sel & 0x01) << CFG_CLK_SEL0) |
|
||
@@ -191,7 +197,7 @@ def __init__(self, dmgr, spi_device, io_update_device=None,
|
||
assert sync_div is None
|
||
sync_div = 0
|
||
|
||
- self.cfg_reg = urukul_cfg(rf_sw=rf_sw, led=0, profile=DEFAULT_PROFILE,
|
||
+ self.cfg_reg = urukul_cfg(rf_sw=rf_sw, led=0, profile=DEFAULT_PROFILE, rb_en=0,
|
||
io_update=0, mask_nu=0, clk_sel=clk_sel,
|
||
sync_sel=sync_sel,
|
||
rst=0, io_rst=0, clk_div=clk_div)
|
||
@@ -226,6 +232,10 @@ def sta_read(self) -> TInt32:
|
||
|
||
:return: The status register value.
|
||
"""
|
||
+ return self.sta_read_impl()
|
||
+
|
||
+ @kernel
|
||
+ def sta_read_impl(self):
|
||
self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, 24,
|
||
SPIT_CFG_RD, CS_CFG)
|
||
self.bus.write(self.cfg_reg << 8)
|
||
diff --git a/artiq/examples/kasli_suservo/device_db.py b/artiq/examples/kasli_suservo/device_db.py
|
||
index c52b82a947..8e9d875205 100644
|
||
--- a/artiq/examples/kasli_suservo/device_db.py
|
||
+++ b/artiq/examples/kasli_suservo/device_db.py
|
||
@@ -142,53 +142,66 @@
|
||
"arguments": {"channel": 15},
|
||
},
|
||
|
||
+ "ttl_urukul0_io_update": {
|
||
+ "type": "local",
|
||
+ "module": "artiq.coredevice.ttl",
|
||
+ "class": "TTLOut",
|
||
+ "arguments": {"channel": 16}
|
||
+ },
|
||
+ "ttl_urukul1_io_update": {
|
||
+ "type": "local",
|
||
+ "module": "artiq.coredevice.ttl",
|
||
+ "class": "TTLOut",
|
||
+ "arguments": {"channel": 17}
|
||
+ },
|
||
+
|
||
"suservo0_ch0": {
|
||
"type": "local",
|
||
"module": "artiq.coredevice.suservo",
|
||
"class": "Channel",
|
||
- "arguments": {"channel": 16, "servo_device": "suservo0"}
|
||
+ "arguments": {"channel": 18, "servo_device": "suservo0"}
|
||
},
|
||
"suservo0_ch1": {
|
||
"type": "local",
|
||
"module": "artiq.coredevice.suservo",
|
||
"class": "Channel",
|
||
- "arguments": {"channel": 17, "servo_device": "suservo0"}
|
||
+ "arguments": {"channel": 19, "servo_device": "suservo0"}
|
||
},
|
||
"suservo0_ch2": {
|
||
"type": "local",
|
||
"module": "artiq.coredevice.suservo",
|
||
"class": "Channel",
|
||
- "arguments": {"channel": 18, "servo_device": "suservo0"}
|
||
+ "arguments": {"channel": 20, "servo_device": "suservo0"}
|
||
},
|
||
"suservo0_ch3": {
|
||
"type": "local",
|
||
"module": "artiq.coredevice.suservo",
|
||
"class": "Channel",
|
||
- "arguments": {"channel": 19, "servo_device": "suservo0"}
|
||
+ "arguments": {"channel": 21, "servo_device": "suservo0"}
|
||
},
|
||
"suservo0_ch4": {
|
||
"type": "local",
|
||
"module": "artiq.coredevice.suservo",
|
||
"class": "Channel",
|
||
- "arguments": {"channel": 20, "servo_device": "suservo0"}
|
||
+ "arguments": {"channel": 22, "servo_device": "suservo0"}
|
||
},
|
||
"suservo0_ch5": {
|
||
"type": "local",
|
||
"module": "artiq.coredevice.suservo",
|
||
"class": "Channel",
|
||
- "arguments": {"channel": 21, "servo_device": "suservo0"}
|
||
+ "arguments": {"channel": 23, "servo_device": "suservo0"}
|
||
},
|
||
"suservo0_ch6": {
|
||
"type": "local",
|
||
"module": "artiq.coredevice.suservo",
|
||
"class": "Channel",
|
||
- "arguments": {"channel": 22, "servo_device": "suservo0"}
|
||
+ "arguments": {"channel": 24, "servo_device": "suservo0"}
|
||
},
|
||
"suservo0_ch7": {
|
||
"type": "local",
|
||
"module": "artiq.coredevice.suservo",
|
||
"class": "Channel",
|
||
- "arguments": {"channel": 23, "servo_device": "suservo0"}
|
||
+ "arguments": {"channel": 25, "servo_device": "suservo0"}
|
||
},
|
||
|
||
"suservo0": {
|
||
@@ -196,7 +209,7 @@
|
||
"module": "artiq.coredevice.suservo",
|
||
"class": "SUServo",
|
||
"arguments": {
|
||
- "channel": 24,
|
||
+ "channel": 26,
|
||
"pgia_device": "spi_sampler0_pgia",
|
||
"cpld_devices": ["urukul0_cpld", "urukul1_cpld"],
|
||
"dds_devices": ["urukul0_dds", "urukul1_dds"],
|
||
@@ -207,33 +220,37 @@
|
||
"type": "local",
|
||
"module": "artiq.coredevice.spi2",
|
||
"class": "SPIMaster",
|
||
- "arguments": {"channel": 25}
|
||
+ "arguments": {"channel": 27}
|
||
},
|
||
|
||
"spi_urukul0": {
|
||
"type": "local",
|
||
"module": "artiq.coredevice.spi2",
|
||
"class": "SPIMaster",
|
||
- "arguments": {"channel": 26}
|
||
+ "arguments": {"channel": 28}
|
||
},
|
||
"urukul0_cpld": {
|
||
"type": "local",
|
||
- "module": "artiq.coredevice.urukul",
|
||
+ "module": "artiq.coredevice.suservo",
|
||
"class": "CPLD",
|
||
"arguments": {
|
||
"spi_device": "spi_urukul0",
|
||
+ "io_update_device": "ttl_urukul0_io_update",
|
||
+ "sync_device": "clkgen_dds_sync_in",
|
||
"refclk": 100e6,
|
||
"clk_sel": 0
|
||
}
|
||
},
|
||
"urukul0_dds": {
|
||
"type": "local",
|
||
- "module": "artiq.coredevice.ad9910",
|
||
+ "module": "artiq.coredevice.suservo",
|
||
"class": "AD9910",
|
||
"arguments": {
|
||
"pll_n": 40,
|
||
"chip_select": 3,
|
||
"cpld_device": "urukul0_cpld",
|
||
+ "io_update_delay": 0,
|
||
+ "sync_delay_seed": -1,
|
||
}
|
||
},
|
||
|
||
@@ -241,26 +258,40 @@
|
||
"type": "local",
|
||
"module": "artiq.coredevice.spi2",
|
||
"class": "SPIMaster",
|
||
- "arguments": {"channel": 27}
|
||
+ "arguments": {"channel": 29}
|
||
},
|
||
"urukul1_cpld": {
|
||
"type": "local",
|
||
- "module": "artiq.coredevice.urukul",
|
||
+ "module": "artiq.coredevice.suservo",
|
||
"class": "CPLD",
|
||
"arguments": {
|
||
"spi_device": "spi_urukul1",
|
||
+ "io_update_device": "ttl_urukul1_io_update",
|
||
+ "sync_device": "clkgen_dds_sync_in",
|
||
"refclk": 100e6,
|
||
"clk_sel": 0
|
||
}
|
||
},
|
||
"urukul1_dds": {
|
||
"type": "local",
|
||
- "module": "artiq.coredevice.ad9910",
|
||
+ "module": "artiq.coredevice.suservo",
|
||
"class": "AD9910",
|
||
"arguments": {
|
||
"pll_n": 40,
|
||
"chip_select": 3,
|
||
"cpld_device": "urukul1_cpld",
|
||
+ "io_update_delay": 0,
|
||
+ "sync_delay_seed": -1,
|
||
+ }
|
||
+ },
|
||
+
|
||
+ "clkgen_dds_sync_in": {
|
||
+ "type": "local",
|
||
+ "module": "artiq.coredevice.ttl",
|
||
+ "class": "TTLClockGen",
|
||
+ "arguments": {
|
||
+ "channel": 30,
|
||
+ "acc_width": 4
|
||
}
|
||
},
|
||
|
||
diff --git a/artiq/frontend/artiq_ddb_template.py b/artiq/frontend/artiq_ddb_template.py
|
||
index b6d9294a37..93a74d46e7 100755
|
||
--- a/artiq/frontend/artiq_ddb_template.py
|
||
+++ b/artiq/frontend/artiq_ddb_template.py
|
||
@@ -424,6 +424,16 @@ def process_suservo(self, rtio_offset, peripheral):
|
||
sampler_name = self.get_name("sampler")
|
||
urukul_names = [self.get_name("urukul") for _ in range(2)]
|
||
channel = count(0)
|
||
+ for urukul_name in urukul_names:
|
||
+ self.gen("""
|
||
+ device_db["ttl_{urukul_name}_io_update"] = {{
|
||
+ "type": "local",
|
||
+ "module": "artiq.coredevice.ttl",
|
||
+ "class": "TTLOut",
|
||
+ "arguments": {{"channel": 0x{ttl_channel:06x}}}
|
||
+ }}""",
|
||
+ urukul_name=urukul_name,
|
||
+ ttl_channel=rtio_offset+next(channel))
|
||
for i in range(8):
|
||
self.gen("""
|
||
device_db["{suservo_name}_ch{suservo_chn}"] = {{
|
||
@@ -472,17 +482,19 @@ def process_suservo(self, rtio_offset, peripheral):
|
||
}}
|
||
device_db["{urukul_name}_cpld"] = {{
|
||
"type": "local",
|
||
- "module": "artiq.coredevice.urukul",
|
||
+ "module": "artiq.coredevice.suservo",
|
||
"class": "CPLD",
|
||
"arguments": {{
|
||
"spi_device": "spi_{urukul_name}",
|
||
+ "io_update_device": "ttl_{urukul_name}_io_update",
|
||
+ "sync_device": "clkgen_{suservo_name}_dds_sync_in",
|
||
"refclk": {refclk},
|
||
"clk_sel": {clk_sel}
|
||
}}
|
||
}}
|
||
device_db["{urukul_name}_dds"] = {{
|
||
"type": "local",
|
||
- "module": "artiq.coredevice.ad9910",
|
||
+ "module": "artiq.coredevice.suservo",
|
||
"class": "AD9910",
|
||
"arguments": {{
|
||
"pll_n": {pll_n},
|
||
@@ -490,12 +502,25 @@ def process_suservo(self, rtio_offset, peripheral):
|
||
"cpld_device": "{urukul_name}_cpld"{pll_vco}
|
||
}}
|
||
}}""",
|
||
+ suservo_name=suservo_name,
|
||
urukul_name=urukul_name,
|
||
urukul_channel=rtio_offset+next(channel),
|
||
refclk=peripheral.get("refclk", self.master_description["rtio_frequency"]),
|
||
clk_sel=peripheral["clk_sel"],
|
||
pll_vco=",\n \"pll_vco\": {}".format(pll_vco) if pll_vco is not None else "",
|
||
pll_n=peripheral["pll_n"])
|
||
+ self.gen("""
|
||
+ device_db["clkgen_{suservo_name}_dds_sync_in"] = {{
|
||
+ "type": "local",
|
||
+ "module": "artiq.coredevice.ttl",
|
||
+ "class": "TTLClockGen",
|
||
+ "arguments": {{
|
||
+ "channel": 0x{clkgen_channel:06x},
|
||
+ "acc_width": 4
|
||
+ }}
|
||
+ }}""",
|
||
+ suservo_name=suservo_name,
|
||
+ clkgen_channel=rtio_offset+next(channel))
|
||
return next(channel)
|
||
|
||
def process_zotino(self, rtio_offset, peripheral):
|
||
diff --git a/artiq/gateware/eem.py b/artiq/gateware/eem.py
|
||
index 467f3cae2e..c7ce7c5879 100644
|
||
--- a/artiq/gateware/eem.py
|
||
+++ b/artiq/gateware/eem.py
|
||
@@ -6,6 +6,7 @@
|
||
from artiq.gateware.rtio.phy import spi2, ad53xx_monitor, dds, grabber
|
||
from artiq.gateware.suservo import servo, pads as servo_pads
|
||
from artiq.gateware.rtio.phy import servo as rtservo, fastino, phaser
|
||
+from artiq.gateware.rtio.phy import ttl_simple
|
||
|
||
|
||
def _eem_signal(i):
|
||
@@ -536,17 +537,17 @@ def add_std(cls, target, eem, eem_aux=None, eem_aux2=None, ttl_out_cls=None,
|
||
class SUServo(_EEM):
|
||
@staticmethod
|
||
def io(*eems, iostandard):
|
||
- assert len(eems) in (4, 6)
|
||
- io = (Sampler.io(*eems[0:2], iostandard=iostandard)
|
||
- + Urukul.io_qspi(*eems[2:4], iostandard=iostandard))
|
||
- if len(eems) == 6: # two Urukuls
|
||
- io += Urukul.io_qspi(*eems[4:6], iostandard=iostandard)
|
||
+ assert len(eems) >= 4 and len(eems) % 2 == 0
|
||
+ io = Sampler.io(*eems[0:2], iostandard=iostandard)
|
||
+ for i in range(len(eems) // 2 - 1):
|
||
+ io += Urukul.io_qspi(*eems[(2 * i + 2):(2 * i + 4)], iostandard=iostandard)
|
||
return io
|
||
|
||
@classmethod
|
||
def add_std(cls, target, eems_sampler, eems_urukul,
|
||
t_rtt=4, clk=1, shift=11, profile=5,
|
||
- iostandard=default_iostandard):
|
||
+ sync_gen_cls=ttl_simple.ClockGen,
|
||
+ iostandard=default_iostandard, sysclk_per_clk=8):
|
||
"""Add a 8-channel Sampler-Urukul Servo
|
||
|
||
:param t_rtt: upper estimate for clock round-trip propagation time from
|
||
@@ -562,6 +563,8 @@ def add_std(cls, target, eems_sampler, eems_urukul,
|
||
(default: 11)
|
||
:param profile: log2 of the number of profiles for each DDS channel
|
||
(default: 5)
|
||
+ :param sysclk_per_clk: DDS "sysclk" (4*refclk = 1GHz typ.) cycles per
|
||
+ FPGA "sys" clock (125MHz typ.) cycles (default: 8)
|
||
"""
|
||
cls.add_extension(
|
||
target, *(eems_sampler + sum(eems_urukul, [])),
|
||
@@ -573,27 +576,29 @@ def add_std(cls, target, eems_sampler, eems_urukul,
|
||
urukul_pads = servo_pads.UrukulPads(
|
||
target.platform, *eem_urukul)
|
||
target.submodules += sampler_pads, urukul_pads
|
||
+ target.rtio_channels.extend(
|
||
+ rtio.Channel.from_phy(phy) for phy in urukul_pads.io_update_phys)
|
||
# timings in units of RTIO coarse period
|
||
adc_p = servo.ADCParams(width=16, channels=8, lanes=4, t_cnvh=4,
|
||
# account for SCK DDR to CONV latency
|
||
# difference (4 cycles measured)
|
||
t_conv=57 - 4, t_rtt=t_rtt + 4)
|
||
iir_p = servo.IIRWidths(state=25, coeff=18, adc=16, asf=14, word=16,
|
||
- accu=48, shift=shift, channel=3,
|
||
- profile=profile, dly=8)
|
||
- dds_p = servo.DDSParams(width=8 + 32 + 16 + 16,
|
||
- channels=adc_p.channels, clk=clk)
|
||
+ accu=48, shift=shift, profile=profile, dly=8)
|
||
+ dds_p = servo.DDSParams(width=8 + 32 + 16 + 16, sysclk_per_clk=sysclk_per_clk,
|
||
+ channels=4*len(eem_urukul), clk=clk)
|
||
su = servo.Servo(sampler_pads, urukul_pads, adc_p, iir_p, dds_p)
|
||
su = ClockDomainsRenamer("rio_phy")(su)
|
||
# explicitly name the servo submodule to enable the migen namer to derive
|
||
# a name for the adc return clock domain
|
||
setattr(target.submodules, "suservo_eem{}".format(eems_sampler[0]), su)
|
||
|
||
- ctrls = [rtservo.RTServoCtrl(ctrl) for ctrl in su.iir.ctrl]
|
||
+ ctrls = [rtservo.RTServoCtrl(ctrl, ctrl_reftime)
|
||
+ for ctrl, ctrl_reftime in zip(su.iir.ctrl, su.iir.ctrl_reftime)]
|
||
target.submodules += ctrls
|
||
target.rtio_channels.extend(
|
||
rtio.Channel.from_phy(ctrl) for ctrl in ctrls)
|
||
- mem = rtservo.RTServoMem(iir_p, su)
|
||
+ mem = rtservo.RTServoMem(iir_p, su, urukul_pads.io_update_phys)
|
||
target.submodules += mem
|
||
target.rtio_channels.append(rtio.Channel.from_phy(mem, ififo_depth=4))
|
||
|
||
@@ -603,27 +608,24 @@ def add_std(cls, target, eems_sampler, eems_urukul,
|
||
target.submodules += phy
|
||
target.rtio_channels.append(rtio.Channel.from_phy(phy, ififo_depth=4))
|
||
|
||
- for i in range(2):
|
||
- if len(eem_urukul) > i:
|
||
- spi_p, spi_n = (
|
||
- target.platform.request("{}_spi_p".format(eem_urukul[i])),
|
||
- target.platform.request("{}_spi_n".format(eem_urukul[i])))
|
||
- else: # create a dummy bus
|
||
- spi_p = Record([("clk", 1), ("cs_n", 1)]) # mosi, cs_n
|
||
- spi_n = None
|
||
-
|
||
+ for eem_urukuli in eem_urukul:
|
||
+ spi_p, spi_n = (
|
||
+ target.platform.request("{}_spi_p".format(eem_urukuli)),
|
||
+ target.platform.request("{}_spi_n".format(eem_urukuli)))
|
||
phy = spi2.SPIMaster(spi_p, spi_n)
|
||
target.submodules += phy
|
||
target.rtio_channels.append(rtio.Channel.from_phy(phy, ififo_depth=4))
|
||
|
||
- for j, eem_urukuli in enumerate(eem_urukul):
|
||
- pads = target.platform.request("{}_dds_reset_sync_in".format(eem_urukuli))
|
||
- target.specials += DifferentialOutput(0, pads.p, pads.n)
|
||
+ if sync_gen_cls is not None: # AD9910 variant and SYNC_IN from EEM
|
||
+ phy = sync_gen_cls(urukul_pads.dds_reset_sync_in, ftw_width=4)
|
||
+ target.submodules += phy
|
||
+ target.rtio_channels.append(rtio.Channel.from_phy(phy))
|
||
|
||
+ for j, eem_urukuli in enumerate(eem_urukul):
|
||
for i, signal in enumerate("sw0 sw1 sw2 sw3".split()):
|
||
pads = target.platform.request("{}_{}".format(eem_urukuli, signal))
|
||
target.specials += DifferentialOutput(
|
||
- su.iir.ctrl[j*4 + i].en_out, pads.p, pads.n)
|
||
+ su.iir.ctrl[j * 4 + i].en_out, pads.p, pads.n)
|
||
|
||
|
||
class Mirny(_EEM):
|
||
diff --git a/artiq/gateware/rtio/phy/servo.py b/artiq/gateware/rtio/phy/servo.py
|
||
index 9fa6345211..0f7ebbf4b2 100644
|
||
--- a/artiq/gateware/rtio/phy/servo.py
|
||
+++ b/artiq/gateware/rtio/phy/servo.py
|
||
@@ -1,25 +1,32 @@
|
||
from migen import *
|
||
-
|
||
from artiq.gateware.rtio import rtlink
|
||
|
||
|
||
class RTServoCtrl(Module):
|
||
"""Per channel RTIO control interface"""
|
||
- def __init__(self, ctrl):
|
||
+ def __init__(self, ctrl, ctrl_reftime):
|
||
self.rtlink = rtlink.Interface(
|
||
- rtlink.OInterface(len(ctrl.profile) + 2))
|
||
+ rtlink.OInterface(
|
||
+ data_width=max(len(ctrl.profile) + 3,
|
||
+ len(ctrl_reftime.sysclks_fine)),
|
||
+ address_width=1)
|
||
+ )
|
||
|
||
# # #
|
||
|
||
+ sel_ref = self.rtlink.o.address[0]
|
||
self.comb += [
|
||
- ctrl.stb.eq(self.rtlink.o.stb),
|
||
- self.rtlink.o.busy.eq(0)
|
||
+ ctrl.stb.eq(self.rtlink.o.stb & ~sel_ref),
|
||
+ self.rtlink.o.busy.eq(0),
|
||
+ ctrl_reftime.stb.eq(self.rtlink.o.stb & sel_ref),
|
||
]
|
||
+ ctrl_cases = {
|
||
+ 0: Cat(ctrl.en_out, ctrl.en_iir, ctrl.en_pt, ctrl.profile).eq(
|
||
+ self.rtlink.o.data),
|
||
+ 1: ctrl_reftime.sysclks_fine.eq(self.rtlink.o.data),
|
||
+ }
|
||
self.sync.rio_phy += [
|
||
- If(self.rtlink.o.stb,
|
||
- Cat(ctrl.en_out, ctrl.en_iir, ctrl.profile).eq(
|
||
- self.rtlink.o.data)
|
||
- )
|
||
+ If(self.rtlink.o.stb, Case(self.rtlink.o.address, ctrl_cases))
|
||
]
|
||
|
||
|
||
@@ -34,35 +41,45 @@ class RTServoMem(Module):
|
||
"""All-channel all-profile coefficient and state RTIO control
|
||
interface.
|
||
|
||
+ The real-time interface exposes the following functions:
|
||
+ 1. enable/disable servo iterations
|
||
+ 2. read the servo status (including state of clip register)
|
||
+ 3. access the IIR coefficient memory (set PI loop gains etc.)
|
||
+ 4. access the IIR state memory (set offset and read ADC data)
|
||
+
|
||
+ The bit assignments for the servo address space are (from MSB):
|
||
+ * write-enable (1 bit)
|
||
+ * sel_coeff (1 bit)
|
||
+ If selected, the coefficient memory location is
|
||
+ addressed by all the lower bits excluding the LSB (high_coeff).
|
||
+ - high_coeff (1 bit) selects between the upper and lower halves of that
|
||
+ memory location.
|
||
+ Else (if ~sel_coeff), the following bits are:
|
||
+ - sel (2 bits) selects between the following memory locations:
|
||
+
|
||
+ destination | sel | sel_coeff |
|
||
+ ----------------|-------|--------------|
|
||
+ IIR coeff mem | - | 1 |
|
||
+ DDS delay mem | 1 | 0 |
|
||
+ IIR state mem | 2 | 0 |
|
||
+ config (write) | 3 | 0 |
|
||
+ status (read) | 3 | 0 |
|
||
+
|
||
+ - IIR state memory address
|
||
+
|
||
Servo internal addresses are internal_address_width wide, which is
|
||
typically longer than the 8-bit RIO address space. We pack the overflow
|
||
onto the RTIO data word after the data.
|
||
|
||
- Servo address space (from LSB):
|
||
- - IIR coefficient/state memory address, (w.profile + w.channel + 2) bits.
|
||
- If the state memory is selected, the lower bits are used directly as
|
||
- the memory address. If the coefficient memory is selected, the LSB
|
||
- (high_coeff) selects between the upper and lower halves of the memory
|
||
- location, which is two coefficients wide, with the remaining bits used
|
||
- as the memory address.
|
||
- - config_sel (1 bit)
|
||
- - state_sel (1 bit)
|
||
- - we (1 bit)
|
||
-
|
||
- destination | config_sel | state_sel
|
||
- ----------------|------------|----------
|
||
- IIR coeff mem | 0 | 0
|
||
- IIR coeff mem | 1 | 0
|
||
- IIR state mem | 0 | 1
|
||
- config (write) | 1 | 1
|
||
- status (read) | 1 | 1
|
||
+ The address layout reflects the fact that typically, the coefficient memory
|
||
+ address is 2 bits wider than the state memory address.
|
||
|
||
Values returned to the user on the Python side of the RTIO interface are
|
||
32 bit, so we sign-extend all values from w.coeff to that width. This works
|
||
(instead of having to decide whether to sign- or zero-extend per address), as
|
||
all unsigned values are less wide than w.coeff.
|
||
"""
|
||
- def __init__(self, w, servo):
|
||
+ def __init__(self, w, servo, io_update_phys):
|
||
m_coeff = servo.iir.m_coeff.get_port(write_capable=True,
|
||
mode=READ_FIRST,
|
||
we_granularity=w.coeff, clock_domain="rio")
|
||
@@ -71,6 +88,7 @@ def __init__(self, w, servo):
|
||
# mode=READ_FIRST,
|
||
clock_domain="rio")
|
||
self.specials += m_state, m_coeff
|
||
+ w_channel = bits_for(len(servo.iir.dds) - 1)
|
||
|
||
# just expose the w.coeff (18) MSBs of state
|
||
assert w.state >= w.coeff
|
||
@@ -83,7 +101,7 @@ def __init__(self, w, servo):
|
||
assert 8 + w.dly < w.coeff
|
||
|
||
# coeff, profile, channel, 2 mems, rw
|
||
- internal_address_width = 3 + w.profile + w.channel + 1 + 1
|
||
+ internal_address_width = 3 + w.profile + w_channel + 1 + 1
|
||
rtlink_address_width = min(8, internal_address_width)
|
||
overflow_address_width = internal_address_width - rtlink_address_width
|
||
self.rtlink = rtlink.Interface(
|
||
@@ -99,7 +117,7 @@ def __init__(self, w, servo):
|
||
# # #
|
||
|
||
config = Signal(w.coeff, reset=0)
|
||
- status = Signal(w.coeff)
|
||
+ status = Signal(len(self.rtlink.i.data))
|
||
pad = Signal(6)
|
||
self.comb += [
|
||
Cat(servo.start).eq(config),
|
||
@@ -109,15 +127,19 @@ def __init__(self, w, servo):
|
||
|
||
assert len(self.rtlink.o.address) + len(self.rtlink.o.data) - w.coeff == (
|
||
1 + # we
|
||
- 1 + # state_sel
|
||
+ 1 + # sel_coeff
|
||
1 + # high_coeff
|
||
len(m_coeff.adr))
|
||
- # ensure that we can fit config/status into the state address space
|
||
+ # ensure that we can fit config/io_dly/status into the state address space
|
||
assert len(self.rtlink.o.address) + len(self.rtlink.o.data) - w.coeff >= (
|
||
1 + # we
|
||
- 1 + # state_sel
|
||
- 1 + # config_sel
|
||
+ 1 + # sel_coeff
|
||
+ 2 + # sel
|
||
len(m_state.adr))
|
||
+ # ensure that IIR state mem addresses are at least 2 bits less wide than
|
||
+ # IIR coeff mem addresses to ensure we can fit SEL after the state mem
|
||
+ # address and before the SEL_COEFF bit.
|
||
+ assert w.profile + w_channel >= 4
|
||
|
||
internal_address = Signal(internal_address_width)
|
||
self.comb += internal_address.eq(Cat(self.rtlink.o.address,
|
||
@@ -127,52 +149,60 @@ def __init__(self, w, servo):
|
||
self.comb += coeff_data.eq(self.rtlink.o.data[:w.coeff])
|
||
|
||
we = internal_address[-1]
|
||
- state_sel = internal_address[-2]
|
||
- config_sel = internal_address[-3]
|
||
+ sel_coeff = internal_address[-2]
|
||
+ sel1 = internal_address[-3]
|
||
+ sel0 = internal_address[-4]
|
||
high_coeff = internal_address[0]
|
||
+ sel = Signal(2)
|
||
self.comb += [
|
||
self.rtlink.o.busy.eq(0),
|
||
+ sel.eq(Mux(sel_coeff, 0, Cat(sel0, sel1))),
|
||
m_coeff.adr.eq(internal_address[1:]),
|
||
m_coeff.dat_w.eq(Cat(coeff_data, coeff_data)),
|
||
- m_coeff.we[0].eq(self.rtlink.o.stb & ~high_coeff &
|
||
- we & ~state_sel),
|
||
- m_coeff.we[1].eq(self.rtlink.o.stb & high_coeff &
|
||
- we & ~state_sel),
|
||
+ m_coeff.we[0].eq(self.rtlink.o.stb & ~high_coeff & we & sel_coeff),
|
||
+ m_coeff.we[1].eq(self.rtlink.o.stb & high_coeff & we & sel_coeff),
|
||
m_state.adr.eq(internal_address),
|
||
m_state.dat_w[w.state - w.coeff:].eq(self.rtlink.o.data),
|
||
- m_state.we.eq(self.rtlink.o.stb & we & state_sel & ~config_sel),
|
||
+ m_state.we.eq(self.rtlink.o.stb & we & (sel == 2)),
|
||
]
|
||
read = Signal()
|
||
- read_state = Signal()
|
||
read_high = Signal()
|
||
- read_config = Signal()
|
||
+ read_sel = Signal(2)
|
||
self.sync.rio += [
|
||
If(read,
|
||
read.eq(0)
|
||
),
|
||
If(self.rtlink.o.stb,
|
||
read.eq(~we),
|
||
- read_state.eq(state_sel),
|
||
+ read_sel.eq(sel),
|
||
read_high.eq(high_coeff),
|
||
- read_config.eq(config_sel),
|
||
)
|
||
]
|
||
+
|
||
+ # I/O update alignment delays
|
||
+ ioup_dlys = Cat(*[phy.fine_ts for phy in io_update_phys])
|
||
+ assert w.coeff >= len(ioup_dlys)
|
||
+
|
||
self.sync.rio_phy += [
|
||
- If(self.rtlink.o.stb & we & state_sel & config_sel,
|
||
+ If(self.rtlink.o.stb & we & (sel == 3),
|
||
config.eq(self.rtlink.o.data)
|
||
),
|
||
- If(read & read_config & read_state,
|
||
+ If(read & (read_sel == 3),
|
||
[_.clip.eq(0) for _ in servo.iir.ctrl]
|
||
- )
|
||
+ ),
|
||
+ If(self.rtlink.o.stb & we & (sel == 1),
|
||
+ ioup_dlys.eq(self.rtlink.o.data)
|
||
+ ),
|
||
]
|
||
+
|
||
+ # read return value by destination
|
||
+ read_acts = Array([
|
||
+ Mux(read_high, m_coeff.dat_r[w.coeff:], m_coeff.dat_r[:w.coeff]),
|
||
+ ioup_dlys,
|
||
+ m_state.dat_r[w.state - w.coeff:],
|
||
+ status
|
||
+ ])
|
||
self.comb += [
|
||
self.rtlink.i.stb.eq(read),
|
||
- _eq_sign_extend(self.rtlink.i.data,
|
||
- Mux(read_state,
|
||
- Mux(read_config,
|
||
- status,
|
||
- m_state.dat_r[w.state - w.coeff:]),
|
||
- Mux(read_high,
|
||
- m_coeff.dat_r[w.coeff:],
|
||
- m_coeff.dat_r[:w.coeff])))
|
||
+ _eq_sign_extend(self.rtlink.i.data, read_acts[read_sel]),
|
||
]
|
||
diff --git a/artiq/gateware/suservo/dds_ser.py b/artiq/gateware/suservo/dds_ser.py
|
||
index 38d1f6d946..cdccfcc98e 100644
|
||
--- a/artiq/gateware/suservo/dds_ser.py
|
||
+++ b/artiq/gateware/suservo/dds_ser.py
|
||
@@ -1,4 +1,5 @@
|
||
import logging
|
||
+from collections import namedtuple
|
||
|
||
from migen import *
|
||
|
||
@@ -6,11 +7,11 @@
|
||
|
||
from . import spi
|
||
|
||
-
|
||
logger = logging.getLogger(__name__)
|
||
|
||
-
|
||
-DDSParams = spi.SPIParams
|
||
+DDSParams = namedtuple("DDSParams", spi.SPIParams._fields + (
|
||
+ "sysclk_per_clk", # DDS_CLK per FPGA system clock
|
||
+))
|
||
|
||
|
||
class DDS(spi.SPISimple):
|
||
diff --git a/artiq/gateware/suservo/iir.py b/artiq/gateware/suservo/iir.py
|
||
index 0ec9bfa093..3fad77a6ea 100644
|
||
--- a/artiq/gateware/suservo/iir.py
|
||
+++ b/artiq/gateware/suservo/iir.py
|
||
@@ -1,6 +1,7 @@
|
||
from collections import namedtuple
|
||
import logging
|
||
from migen import *
|
||
+from migen.genlib.coding import Encoder
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
@@ -16,7 +17,6 @@
|
||
"word", # "word" size to break up DDS profile data (16)
|
||
"asf", # unsigned amplitude scale factor for DDS (14)
|
||
"shift", # fixed point scaling coefficient for a1, b0, b1 (log2!) (11)
|
||
- "channel", # channels (log2!) (3)
|
||
"profile", # profiles per channel (log2!) (5)
|
||
"dly", # the activation delay
|
||
])
|
||
@@ -99,14 +99,14 @@ class IIR(Module):
|
||
This module implements a multi-channel IIR (infinite impulse response)
|
||
filter processor optimized for synthesis on FPGAs.
|
||
|
||
- The module is parametrized by passing a ``IIRWidths()`` object which
|
||
- will be abbreviated W here.
|
||
+ The module is parametrized by passing a ``IIRWidths()`` object, and
|
||
+ two more objects which will be abbreviated W, W_O and W_I here.
|
||
|
||
- It reads 1 << W.channels input channels (typically from an ADC)
|
||
+ It reads W_I.channels input channels (typically from an ADC)
|
||
and on each iteration processes the data using a first-order IIR filter.
|
||
At the end of the cycle each the output of the filter together with
|
||
additional data (typically frequency tunning word and phase offset word
|
||
- for a DDS) are presented at the 1 << W.channels outputs of the module.
|
||
+ for a DDS) are presented at the W_O.channels outputs of the module.
|
||
|
||
Profile memory
|
||
==============
|
||
@@ -145,10 +145,10 @@ class IIR(Module):
|
||
-------------
|
||
|
||
The state memory holds all Y1 values (IIR processor outputs) for all
|
||
- profiles of all channels in the lower half (1 << W.profile + W.channel
|
||
- addresses) and the pairs of old and new ADC input values X1, and X0,
|
||
- in the upper half (1 << W.channel addresses). Each memory location is
|
||
- W.state bits wide.
|
||
+ profiles of all channels in the lower half (1 << W.profile)*W_O.channels
|
||
+ addresses, and the pairs of old and new ADC input values X1, and X0,
|
||
+ in the upper half (W_I.channels addresses).
|
||
+ Each memory location is W.state bits wide.
|
||
|
||
Real-time control
|
||
=================
|
||
@@ -157,15 +157,16 @@ class IIR(Module):
|
||
|
||
* The active profile, PROFILE
|
||
* Whether to perform IIR filter iterations, EN_IIR
|
||
+ * Whether to track the DDS phase coherently, EN_PT
|
||
* The RF switch state enabling output from the channel, EN_OUT
|
||
|
||
Delayed IIR processing
|
||
======================
|
||
|
||
- The IIR filter iterations on a given channel are only performed all of the
|
||
- following are true:
|
||
+ The IIR filter iterations on a given channel are only performed if all of
|
||
+ the following are true:
|
||
|
||
- * PROFILE, EN_IIR, EN_OUT have not been updated in the within the
|
||
+ * PROFILE, EN_IIR, EN_OUT have not been updated within the
|
||
last DLY cycles
|
||
* EN_IIR is asserted
|
||
* EN_OUT is asserted
|
||
@@ -176,9 +177,8 @@ class IIR(Module):
|
||
Typical design at the DSP level. This does not include the description of
|
||
the pipelining or the overall latency involved.
|
||
|
||
- IIRWidths(state=25, coeff=18, adc=16,
|
||
- asf=14, word=16, accu=48, shift=11,
|
||
- channel=3, profile=5, dly=8)
|
||
+ IIRWidths(state=25, coeff=18, adc=16, asf=14,
|
||
+ word=16, accu=48, shift=11, profile=5, dly=8)
|
||
|
||
X0 = ADC * 2^(25 - 1 - 16)
|
||
X1 = X0 delayed by one cycle
|
||
@@ -213,39 +213,64 @@ class IIR(Module):
|
||
--/--: signal with a given bit width always includes a sign bit
|
||
-->--: flow is to the right and down unless otherwise indicated
|
||
"""
|
||
- def __init__(self, w):
|
||
- self.widths = w
|
||
- for i, j in enumerate(w):
|
||
- assert j > 0, (i, j, w)
|
||
+ def __init__(self, w, w_i, w_o, t_cycle):
|
||
+ for v in (w, w_i, w_o):
|
||
+ for i, j in enumerate(v):
|
||
+ assert j > 0, (i, j, v)
|
||
assert w.word <= w.coeff # same memory
|
||
assert w.state + w.coeff + 3 <= w.accu
|
||
|
||
+ # Reference counter for coherent phase tracking (we assume this doesn't
|
||
+ # roll over – a good assumption, as the period is, for a typical clock
|
||
+ # frequency, 2^48 / 125 MHz = ~26 days).
|
||
+ self.t_running = Signal(48, reset_less=True)
|
||
+
|
||
+ # If true, internal DDS phase tracking state is reset, matching DDS
|
||
+ # chips with phase cleared (and zero FTW) before the start of the
|
||
+ # iteration. Automatically reset at the end of the iteration.
|
||
+ self.reset_dds_phase = Signal()
|
||
+
|
||
# m_coeff of active profiles should only be accessed externally during
|
||
# ~processing
|
||
self.specials.m_coeff = Memory(
|
||
width=2*w.coeff, # Cat(pow/ftw/offset, cfg/a/b)
|
||
- depth=4 << w.profile + w.channel)
|
||
+ depth=(4 << w.profile) * w_o.channels)
|
||
# m_state[x] should only be read externally during ~(shifting | loading)
|
||
# m_state[y] of active profiles should only be read externally during
|
||
# ~processing
|
||
self.specials.m_state = Memory(
|
||
width=w.state, # y1,x0,x1
|
||
- depth=(1 << w.profile + w.channel) + (2 << w.channel))
|
||
+ depth=(1 << w.profile) * w_o.channels + 2 * w_i.channels)
|
||
# ctrl should only be updated synchronously
|
||
self.ctrl = [Record([
|
||
("profile", w.profile),
|
||
("en_out", 1),
|
||
("en_iir", 1),
|
||
+ ("en_pt", 1),
|
||
("clip", 1),
|
||
("stb", 1)])
|
||
- for i in range(1 << w.channel)]
|
||
+ for i in range(w_o.channels)]
|
||
+ # "Shadow copy" of phase accumulator in DDS accumulator for each output
|
||
+ # channel.
|
||
+ self.specials.m_accum_ftw = Memory(
|
||
+ width=2 * w.word,
|
||
+ depth=w_o.channels)
|
||
+ # ctrl_reftime should only be updated synchronously
|
||
+ self.ctrl_reftime = [Record([
|
||
+ ("sysclks_fine", bits_for(w_o.sysclk_per_clk - 1)),
|
||
+ ("stb", 1)])
|
||
+ for i in range(w_o.channels)]
|
||
+ # Reference time for each output channel.
|
||
+ self.specials.m_t_ref = Memory(
|
||
+ width=len(self.t_running),
|
||
+ depth=w_o.channels)
|
||
# only update during ~loading
|
||
self.adc = [Signal((w.adc, True), reset_less=True)
|
||
- for i in range(1 << w.channel)]
|
||
+ for i in range(w_i.channels)]
|
||
# Cat(ftw0, ftw1, pow, asf)
|
||
# only read externally during ~processing
|
||
- self.dds = [Signal(4*w.word, reset_less=True)
|
||
- for i in range(1 << w.channel)]
|
||
+ self.dds = [Signal(4 * w.word, reset_less=True)
|
||
+ for i in range(w_o.channels)]
|
||
# perform one IIR iteration, start with loading,
|
||
# then processing, then shifting, end with done
|
||
self.start = Signal()
|
||
@@ -265,8 +290,15 @@ def __init__(self, w):
|
||
profiles = Array([ch.profile for ch in self.ctrl])
|
||
en_outs = Array([ch.en_out for ch in self.ctrl])
|
||
en_iirs = Array([ch.en_iir for ch in self.ctrl])
|
||
+ en_pts = Array([ch.en_pt for ch in self.ctrl])
|
||
clips = Array([ch.clip for ch in self.ctrl])
|
||
|
||
+ # Sample of the reference counter at the start of the current iteration,
|
||
+ # such that a common reference time is used for phase calculations
|
||
+ # across all channels, in DDS sysclk units.
|
||
+ sysclks_to_iter_start = Signal(
|
||
+ len(self.t_running) + bits_for(w_o.sysclk_per_clk - 1))
|
||
+
|
||
# Main state machine sequencing the steps of each servo iteration. The
|
||
# module IDLEs until self.start is asserted, and then runs through LOAD,
|
||
# PROCESS and SHIFT in order (see description of corresponding flags
|
||
@@ -281,7 +313,7 @@ def __init__(self, w):
|
||
# using the (MSBs of) t_current_step, and, after all channels have been
|
||
# covered, proceed once the pipeline has completely drained.
|
||
self.submodules.fsm = fsm = FSM("IDLE")
|
||
- t_current_step = Signal(w.channel + 2)
|
||
+ t_current_step = Signal(max=max(4 * (w_o.channels + 2), 2 * w_i.channels))
|
||
t_current_step_clr = Signal()
|
||
|
||
# pipeline group activity flags (SR)
|
||
@@ -293,12 +325,13 @@ def __init__(self, w):
|
||
self.done.eq(1),
|
||
t_current_step_clr.eq(1),
|
||
If(self.start,
|
||
+ NextValue(sysclks_to_iter_start, self.t_running * w_o.sysclk_per_clk),
|
||
NextState("LOAD")
|
||
)
|
||
)
|
||
fsm.act("LOAD",
|
||
self.loading.eq(1),
|
||
- If(t_current_step == (1 << w.channel) - 1,
|
||
+ If(t_current_step == w_i.channels - 1,
|
||
t_current_step_clr.eq(1),
|
||
NextValue(stages_active[0], 1),
|
||
NextState("PROCESS")
|
||
@@ -311,11 +344,12 @@ def __init__(self, w):
|
||
If(stages_active == 0,
|
||
t_current_step_clr.eq(1),
|
||
NextState("SHIFT"),
|
||
+ NextValue(self.reset_dds_phase, 0)
|
||
)
|
||
)
|
||
fsm.act("SHIFT",
|
||
self.shifting.eq(1),
|
||
- If(t_current_step == (2 << w.channel) - 1,
|
||
+ If(t_current_step == 2 * w_i.channels - 1,
|
||
NextState("IDLE")
|
||
)
|
||
)
|
||
@@ -333,13 +367,13 @@ def __init__(self, w):
|
||
# pipeline group channel pointer (SR)
|
||
# for each pipeline stage, this is the channel currently being
|
||
# processed
|
||
- channel = [Signal(w.channel, reset_less=True) for i in range(3)]
|
||
+ channel = [Signal(max=w_o.channels, reset_less=True) for i in range(3)]
|
||
self.comb += Cat(pipeline_phase, channel[0]).eq(t_current_step)
|
||
self.sync += [
|
||
If(pipeline_phase == 3,
|
||
Cat(channel[1:]).eq(Cat(channel[:-1])),
|
||
stages_active[1:].eq(stages_active[:-1]),
|
||
- If(channel[0] == (1 << w.channel) - 1,
|
||
+ If(channel[0] == w_o.channels - 1,
|
||
stages_active[0].eq(0)
|
||
)
|
||
)
|
||
@@ -393,13 +427,13 @@ def __init__(self, w):
|
||
|
||
# selected adc and profile delay (combinatorial from dat_r)
|
||
# both share the same coeff word (sel in the lower 8 bits)
|
||
- sel_profile = Signal(w.channel)
|
||
+ sel_profile = Signal(max=w_i.channels)
|
||
dly_profile = Signal(w.dly)
|
||
- assert w.channel <= 8
|
||
+ assert w_o.channels < (1 << 8)
|
||
assert 8 + w.dly <= w.coeff
|
||
|
||
# latched adc selection
|
||
- sel = Signal(w.channel, reset_less=True)
|
||
+ sel = Signal(max=w_i.channels, reset_less=True)
|
||
# iir enable SR
|
||
en = Signal(2, reset_less=True)
|
||
|
||
@@ -407,12 +441,12 @@ def __init__(self, w):
|
||
sel_profile.eq(m_coeff.dat_r[w.coeff:]),
|
||
dly_profile.eq(m_coeff.dat_r[w.coeff + 8:]),
|
||
If(self.shifting,
|
||
- m_state.adr.eq(t_current_step | (1 << w.profile + w.channel)),
|
||
+ m_state.adr.eq(t_current_step + (1 << w.profile) * w_o.channels),
|
||
m_state.dat_w.eq(m_state.dat_r),
|
||
m_state.we.eq(t_current_step[0])
|
||
),
|
||
If(self.loading,
|
||
- m_state.adr.eq((t_current_step << 1) | (1 << w.profile + w.channel)),
|
||
+ m_state.adr.eq((t_current_step << 1) + (1 << w.profile) * w_o.channels),
|
||
m_state.dat_w[-w.adc - 1:-1].eq(Array(self.adc)[t_current_step]),
|
||
m_state.dat_w[-1].eq(m_state.dat_w[-2]),
|
||
m_state.we.eq(1)
|
||
@@ -424,9 +458,9 @@ def __init__(self, w):
|
||
# read old y
|
||
Cat(profile[0], channel[0]),
|
||
# read x0 (recent)
|
||
- 0 | (sel_profile << 1) | (1 << w.profile + w.channel),
|
||
+ 0 | (sel_profile << 1) + (1 << w.profile) * w_o.channels,
|
||
# read x1 (old)
|
||
- 1 | (sel << 1) | (1 << w.profile + w.channel),
|
||
+ 1 | (sel << 1) + (1 << w.profile) * w_o.channels,
|
||
])[pipeline_phase]),
|
||
m_state.dat_w.eq(dsp.output),
|
||
m_state.we.eq((pipeline_phase == 0) & stages_active[2] & en[1]),
|
||
@@ -438,11 +472,9 @@ def __init__(self, w):
|
||
#
|
||
|
||
# internal channel delay counters
|
||
- dlys = Array([Signal(w.dly)
|
||
- for i in range(1 << w.channel)])
|
||
- self._dlys = dlys # expose for debugging only
|
||
+ dlys = Array([Signal(w.dly) for i in range(w_o.channels)])
|
||
|
||
- for i in range(1 << w.channel):
|
||
+ for i in range(w_o.channels):
|
||
self.sync += [
|
||
# (profile != profile_old) | ~en_out
|
||
If(self.ctrl[i].stb,
|
||
@@ -482,25 +514,81 @@ def __init__(self, w):
|
||
}),
|
||
]
|
||
|
||
+ # Update coarse reference time from t_running upon ctrl_reftime strobe
|
||
+ ref_stb_encoder = Encoder(w_o.channels)
|
||
+ m_t_ref_stb = self.m_t_ref.get_port(write_capable=True)
|
||
+ self.specials += m_t_ref_stb
|
||
+ self.submodules += ref_stb_encoder
|
||
+ self.comb += [
|
||
+ ref_stb_encoder.i.eq(Cat([ch.stb for ch in self.ctrl_reftime])),
|
||
+ m_t_ref_stb.adr.eq(ref_stb_encoder.o),
|
||
+ m_t_ref_stb.we.eq(~ref_stb_encoder.n),
|
||
+ m_t_ref_stb.dat_w.eq(self.t_running),
|
||
+ ]
|
||
+
|
||
#
|
||
- # Update DDS profile with FTW/POW/ASF
|
||
- # Stage 0 loads the POW, stage 1 the FTW, and stage 2 writes
|
||
- # the ASF computed by the IIR filter.
|
||
+ # Update DDS profile with FTW/POW/ASF (including phase tracking, if
|
||
+ # enabled). Stage 0 loads the POW, stage 1 the FTW, and stage 2 writes
|
||
+ # the ASF computed by the IIR filter (and adds any phase correction).
|
||
#
|
||
|
||
# muxing
|
||
ddss = Array(self.dds)
|
||
+ sysclks_ref_fine = Array([ch.sysclks_fine for ch in self.ctrl_reftime])
|
||
+
|
||
+ # registered copy of FTW on channel[1]
|
||
+ current_ftw = Signal(2 * w.word, reset_less=True)
|
||
+ # target effective DDS phase (accumulator + POW) at the coming io_update
|
||
+ target_dds_phase = Signal.like(current_ftw)
|
||
+ # DDS-internal phase accumulated until the coming io_update
|
||
+ accum_dds_phase = Signal.like(current_ftw)
|
||
+ # correction to add to the bare POW to yield a phase-coherent DDS output
|
||
+ correcting_pow = Signal(w.word, reset_less=True)
|
||
+ # sum of all FTWs on channel[1], updated with current FTW during the
|
||
+ # calculation
|
||
+ accum_ftw = Signal.like(current_ftw)
|
||
+ # sum of previous FTWs on channel[1] (or 0 on phase coherence reference
|
||
+ # reset)
|
||
+ prev_accum_ftw = Signal.like(current_ftw)
|
||
+ # time since reference time at coming io_update in DDS sysclk units
|
||
+ sysclks_to_ref = Signal.like(sysclks_to_iter_start)
|
||
+ # t_ref in DDS sysclk units
|
||
+ sysclks_ref_to_iter_start = Signal.like(sysclks_to_iter_start)
|
||
+
|
||
+ m_t_ref = self.m_t_ref.get_port()
|
||
+ m_accum_ftw = self.m_accum_ftw.get_port(write_capable=True, mode=READ_FIRST)
|
||
+ self.specials += m_accum_ftw, m_t_ref
|
||
+ prev_accum_ftw = Signal.like(accum_ftw)
|
||
+ self.comb += [
|
||
+ prev_accum_ftw.eq(Mux(self.reset_dds_phase, 0, m_accum_ftw.dat_r)),
|
||
+ m_accum_ftw.adr.eq(channel[1]),
|
||
+ m_accum_ftw.we.eq((pipeline_phase == 3) & stages_active[1]),
|
||
+ m_accum_ftw.dat_w.eq(accum_ftw),
|
||
+ m_t_ref.adr.eq(channel[0]),
|
||
+ ]
|
||
|
||
+ sysclks_per_iter = t_cycle * w_o.sysclk_per_clk
|
||
self.sync += [
|
||
Case(pipeline_phase, {
|
||
0: [
|
||
If(stages_active[1],
|
||
ddss[channel[1]][:w.word].eq(m_coeff.dat_r), # ftw0
|
||
+ current_ftw[:w.word].eq(m_coeff.dat_r),
|
||
+ sysclks_ref_to_iter_start.eq(m_t_ref.dat_r * w_o.sysclk_per_clk),
|
||
+ ),
|
||
+ If(stages_active[2] & en_pts[channel[2]],
|
||
+ # add pow correction if phase tracking enabled
|
||
+ ddss[channel[2]][2*w.word:3*w.word].eq(
|
||
+ ddss[channel[2]][2*w.word:3*w.word] + correcting_pow),
|
||
),
|
||
],
|
||
1: [
|
||
If(stages_active[1],
|
||
ddss[channel[1]][w.word:2 * w.word].eq(m_coeff.dat_r), # ftw1
|
||
+ current_ftw[w.word:].eq(m_coeff.dat_r),
|
||
+ sysclks_to_ref.eq(sysclks_to_iter_start - (
|
||
+ sysclks_ref_to_iter_start + sysclks_ref_fine[channel[1]])),
|
||
+ accum_dds_phase.eq(prev_accum_ftw * sysclks_per_iter),
|
||
),
|
||
If(stages_active[2],
|
||
ddss[channel[2]][3*w.word:].eq( # asf
|
||
@@ -509,14 +597,40 @@ def __init__(self, w):
|
||
],
|
||
2: [
|
||
If(stages_active[0],
|
||
- ddss[channel[0]][2*w.word:3*w.word].eq(m_coeff.dat_r), # pow
|
||
+ # Load bare POW from profile memory.
|
||
+ ddss[channel[0]][2*w.word:3*w.word].eq(m_coeff.dat_r),
|
||
+ ),
|
||
+ If(stages_active[1],
|
||
+ target_dds_phase.eq(current_ftw * sysclks_to_ref),
|
||
+ accum_ftw.eq(prev_accum_ftw + current_ftw),
|
||
),
|
||
],
|
||
3: [
|
||
+ If(stages_active[1],
|
||
+ # Prepare most-significant word to add to POW from
|
||
+ # profile for phase tracking.
|
||
+ correcting_pow.eq(
|
||
+ (target_dds_phase - accum_dds_phase)[w.word:]),
|
||
+ ),
|
||
],
|
||
}),
|
||
]
|
||
|
||
+ # expose for simulation and debugging only
|
||
+ self.widths = w
|
||
+ self.widths_adc = w_i
|
||
+ self.widths_dds = w_o
|
||
+ self.t_cycle = t_cycle
|
||
+ self._state = t_current_step
|
||
+ self._stages = stages_active
|
||
+ self._dt_start = sysclks_to_iter_start
|
||
+ self._sysclks_to_ref = sysclks_to_ref
|
||
+ self._sysclks_ref_to_iter_start = sysclks_ref_to_iter_start
|
||
+ self._sysclks_ref_fine = sysclks_ref_fine
|
||
+ self._ph_acc = accum_dds_phase
|
||
+ self._ph_coh = target_dds_phase
|
||
+ self._dlys = dlys
|
||
+
|
||
def _coeff(self, channel, profile, coeff):
|
||
"""Return ``high_word``, ``address`` and bit ``mask`` for the
|
||
storage of coefficient name ``coeff`` in profile ``profile``
|
||
@@ -564,35 +678,45 @@ def get_coeff(self, channel, profile, coeff):
|
||
def set_state(self, channel, val, profile=None, coeff="y1"):
|
||
"""Set a state value."""
|
||
w = self.widths
|
||
+ w_o = self.widths_dds
|
||
if coeff == "y1":
|
||
assert profile is not None
|
||
yield self.m_state[profile | (channel << w.profile)].eq(val)
|
||
elif coeff == "x0":
|
||
assert profile is None
|
||
- yield self.m_state[(channel << 1) |
|
||
- (1 << w.profile + w.channel)].eq(val)
|
||
+ yield self.m_state[(channel << 1) +
|
||
+ (1 << w.profile) * w_o.channels].eq(val)
|
||
elif coeff == "x1":
|
||
assert profile is None
|
||
- yield self.m_state[1 | (channel << 1) |
|
||
- (1 << w.profile + w.channel)].eq(val)
|
||
+ yield self.m_state[1 | (channel << 1) +
|
||
+ (1 << w.profile) * w_o.channels].eq(val)
|
||
else:
|
||
raise ValueError("no such state", coeff)
|
||
|
||
def get_state(self, channel, profile=None, coeff="y1"):
|
||
"""Get a state value."""
|
||
w = self.widths
|
||
+ w_o = self.widths_dds
|
||
if coeff == "y1":
|
||
val = yield self.m_state[profile | (channel << w.profile)]
|
||
elif coeff == "x0":
|
||
- val = yield self.m_state[(channel << 1) |
|
||
- (1 << w.profile + w.channel)]
|
||
+ val = yield self.m_state[(channel << 1) +
|
||
+ (1 << w.profile) * w_o.channels]
|
||
elif coeff == "x1":
|
||
- val = yield self.m_state[1 | (channel << 1) |
|
||
- (1 << w.profile + w.channel)]
|
||
+ val = yield self.m_state[1 | (channel << 1) +
|
||
+ (1 << w.profile) * w_o.channels]
|
||
else:
|
||
raise ValueError("no such state", coeff)
|
||
return signed(val, w.state)
|
||
|
||
+ def get_accum_ftw(self, channel):
|
||
+ val = yield self.m_accum_ftw[channel]
|
||
+ return val
|
||
+
|
||
+ def get_t_ref(self, channel):
|
||
+ val = yield self.m_t_ref[channel]
|
||
+ return val
|
||
+
|
||
def fast_iter(self):
|
||
"""Perform a single processing iteration."""
|
||
assert (yield self.done)
|
||
@@ -607,6 +731,8 @@ def check_iter(self):
|
||
"""Perform a single processing iteration while verifying
|
||
the behavior."""
|
||
w = self.widths
|
||
+ w_i = self.widths_adc
|
||
+ w_o = self.widths_dds
|
||
|
||
while not (yield self.done):
|
||
yield
|
||
@@ -622,25 +748,33 @@ def check_iter(self):
|
||
|
||
x0s = []
|
||
# check adc loading
|
||
- for i in range(1 << w.channel):
|
||
+ for i in range(w_i.channels):
|
||
v_adc = signed((yield self.adc[i]), w.adc)
|
||
x0 = yield from self.get_state(i, coeff="x0")
|
||
x0s.append(x0)
|
||
- assert v_adc << (w.state - w.adc - 1) == x0, (hex(v_adc), hex(x0))
|
||
logger.debug("adc[%d] adc=%x x0=%x", i, v_adc, x0)
|
||
+ assert v_adc << (w.state - w.adc - 1) == x0, (hex(v_adc), hex(x0))
|
||
|
||
data = []
|
||
# predict output
|
||
- for i in range(1 << w.channel):
|
||
+ for i in range(w_o.channels):
|
||
+ t0 = yield self._dt_start
|
||
+ dds_ftw_accu = yield from self.get_accum_ftw(i)
|
||
+ sysclks_ref = (yield from self.get_t_ref(i)) * self.widths_dds.sysclk_per_clk\
|
||
+ + (yield self.ctrl_reftime[i].sysclks_fine)
|
||
+ logger.debug("dt_start=%d dt_ref=%d t_cycle=%d ftw_accu=%#x",
|
||
+ t0, sysclks_ref, self.t_cycle, dds_ftw_accu)
|
||
+
|
||
j = yield self.ctrl[i].profile
|
||
en_iir = yield self.ctrl[i].en_iir
|
||
en_out = yield self.ctrl[i].en_out
|
||
+ en_pt = yield self.ctrl[i].en_pt
|
||
dly_i = yield self._dlys[i]
|
||
- logger.debug("ctrl[%d] profile=%d en_iir=%d en_out=%d dly=%d",
|
||
- i, j, en_iir, en_out, dly_i)
|
||
+ logger.debug("ctrl[%d] profile=%d en_iir=%d en_out=%d en_pt=%d dly=%d",
|
||
+ i, j, en_iir, en_out, en_pt, dly_i)
|
||
|
||
cfg = yield from self.get_coeff(i, j, "cfg")
|
||
- k_j = cfg & ((1 << w.channel) - 1)
|
||
+ k_j = cfg & ((1 << bits_for(w_i.channels - 1)) - 1)
|
||
dly_j = (cfg >> 8) & 0xff
|
||
logger.debug("cfg[%d,%d] sel=%d dly=%d", i, j, k_j, dly_j)
|
||
|
||
@@ -657,9 +791,13 @@ def check_iter(self):
|
||
|
||
ftw0 = yield from self.get_coeff(i, j, "ftw0")
|
||
ftw1 = yield from self.get_coeff(i, j, "ftw1")
|
||
- pow = yield from self.get_coeff(i, j, "pow")
|
||
- logger.debug("dds[%d,%d] ftw0=%#x ftw1=%#x pow=%#x",
|
||
- i, j, ftw0, ftw1, pow)
|
||
+ _pow = yield from self.get_coeff(i, j, "pow")
|
||
+ ph_coh = ((ftw0 | (ftw1 << w.word)) * (t0 - sysclks_ref))
|
||
+ ph_accu = dds_ftw_accu * self.t_cycle * self.widths_dds.sysclk_per_clk
|
||
+ ph = ph_coh - ph_accu
|
||
+ pow = (_pow + (ph >> w.word)) & 0xffff if en_pt else _pow
|
||
+ logger.debug("dds[%d,%d] ftw0=%#x ftw1=%#x ph_coh=%#x _pow=%#x pow=%#x",
|
||
+ i, j, ftw0, ftw1, ph_coh, _pow, pow)
|
||
|
||
y1 = yield from self.get_state(i, j, "y1")
|
||
x1 = yield from self.get_state(k_j, coeff="x1")
|
||
@@ -681,6 +819,10 @@ def check_iter(self):
|
||
# wait for output
|
||
assert (yield self.processing)
|
||
while (yield self.processing):
|
||
+ logger.debug("sysclks_to_ref=%d sysclks_ref_to_iter_start=%d",
|
||
+ (yield self._sysclks_to_ref),
|
||
+ (yield self._sysclks_ref_to_iter_start))
|
||
+ # logger.debug("%d %d %d %d", *[x for x in (yield self._sysclks_ref_fine)])
|
||
yield
|
||
|
||
assert (yield self.shifting)
|
||
@@ -694,7 +836,7 @@ def check_iter(self):
|
||
logger.debug("adc[%d] x0=%x x1=%x", i, x0, x1)
|
||
|
||
# check new state
|
||
- for i in range(1 << w.channel):
|
||
+ for i in range(w_o.channels):
|
||
j = yield self.ctrl[i].profile
|
||
logger.debug("ch[%d] profile=%d", i, j)
|
||
y1 = yield from self.get_state(i, j, "y1")
|
||
@@ -702,7 +844,7 @@ def check_iter(self):
|
||
assert y1 == y0, (hex(y1), hex(y0))
|
||
|
||
# check dds output
|
||
- for i in range(1 << w.channel):
|
||
+ for i in range(w_o.channels):
|
||
ftw0, ftw1, pow, y0, x1, x0 = data[i]
|
||
asf = y0 >> (w.state - w.asf - 1)
|
||
dds = (ftw0 | (ftw1 << w.word) |
|
||
diff --git a/artiq/gateware/suservo/pads.py b/artiq/gateware/suservo/pads.py
|
||
index 0ab7d352f1..bdae8ee35c 100644
|
||
--- a/artiq/gateware/suservo/pads.py
|
||
+++ b/artiq/gateware/suservo/pads.py
|
||
@@ -1,5 +1,7 @@
|
||
from migen import *
|
||
from migen.genlib.io import DifferentialOutput, DifferentialInput, DDROutput
|
||
+from artiq.gateware.rtio.phy import ttl_serdes_7series, ttl_serdes_generic
|
||
+from artiq.gateware.rtio import rtlink
|
||
|
||
|
||
class SamplerPads(Module):
|
||
@@ -57,27 +59,85 @@ def __init__(self, platform, eem):
|
||
clk=dp.clkout, port=sdop)
|
||
|
||
|
||
+class OutIoUpdate_8X(Module):
|
||
+ def __init__(self, pad):
|
||
+ serdes = ttl_serdes_7series._OSERDESE2_8X()
|
||
+ self.submodules += serdes
|
||
+
|
||
+ self.passthrough = Signal()
|
||
+ self.data = Signal()
|
||
+ self.fine_ts = Signal(3)
|
||
+
|
||
+ self.rtlink = rtlink.Interface(
|
||
+ rtlink.OInterface(1, fine_ts_width=3))
|
||
+ self.probes = [serdes.o[-1]]
|
||
+ override_en = Signal()
|
||
+ override_o = Signal()
|
||
+ self.overrides = [override_en, override_o]
|
||
+
|
||
+ # # #
|
||
+
|
||
+ self.specials += Instance("IOBUFDS",
|
||
+ i_I=serdes.ser_out,
|
||
+ i_T=serdes.t_out,
|
||
+ io_IO=pad.p,
|
||
+ io_IOB=pad.n)
|
||
+
|
||
+ # Just strobe always in non-passthrough mode, as self.data is supposed
|
||
+ # to be always valid.
|
||
+ self.submodules += ttl_serdes_generic._SerdesDriver(
|
||
+ serdes.o,
|
||
+ Mux(self.passthrough, self.rtlink.o.stb, 1),
|
||
+ Mux(self.passthrough, self.rtlink.o.data, self.data),
|
||
+ Mux(self.passthrough, self.rtlink.o.fine_ts, self.fine_ts),
|
||
+ override_en, override_o)
|
||
+
|
||
+ self.comb += self.rtlink.o.busy.eq(~self.passthrough)
|
||
+
|
||
+
|
||
class UrukulPads(Module):
|
||
def __init__(self, platform, *eems):
|
||
spip, spin = [[
|
||
platform.request("{}_qspi_{}".format(eem, pol), 0)
|
||
for eem in eems] for pol in "pn"]
|
||
- ioup = [platform.request("{}_io_update".format(eem), 0)
|
||
- for eem in eems]
|
||
+
|
||
self.cs_n = Signal()
|
||
self.clk = Signal()
|
||
self.io_update = Signal()
|
||
+ self.passthrough = Signal()
|
||
+ self.dds_reset_sync_in = Signal(reset=0) # sync_in phy (one for all)
|
||
+
|
||
+ # # #
|
||
+
|
||
+ self.io_update_phys = []
|
||
+ for eem in eems:
|
||
+ phy = OutIoUpdate_8X(platform.request("{}_io_update".format(eem), 0))
|
||
+ self.io_update_phys.append(phy)
|
||
+ setattr(self.submodules, "{}_io_update_phy".format(eem), phy)
|
||
+ self.comb += [
|
||
+ phy.data.eq(self.io_update),
|
||
+ phy.passthrough.eq(self.passthrough),
|
||
+ ]
|
||
+
|
||
+ sync_in_pads = platform.request("{}_dds_reset_sync_in".format(eem))
|
||
+ sync_in_r = Signal()
|
||
+ self.sync.rio_phy += sync_in_r.eq(self.dds_reset_sync_in)
|
||
+ sync_in_o = Signal()
|
||
+ self.specials += Instance("ODDR",
|
||
+ p_DDR_CLK_EDGE="SAME_EDGE",
|
||
+ i_C=ClockSignal("rio_phy"), i_CE=1, i_S=0, i_R=0,
|
||
+ i_D1=sync_in_r, i_D2=sync_in_r, o_Q=sync_in_o)
|
||
+ self.specials += DifferentialOutput(sync_in_o, sync_in_pads.p, sync_in_pads.n)
|
||
+
|
||
self.specials += [(
|
||
DifferentialOutput(~self.cs_n, spip[i].cs, spin[i].cs),
|
||
- DifferentialOutput(self.clk, spip[i].clk, spin[i].clk),
|
||
- DifferentialOutput(self.io_update, ioup[i].p, ioup[i].n))
|
||
+ DifferentialOutput(self.clk, spip[i].clk, spin[i].clk))
|
||
for i in range(len(eems))]
|
||
- for i in range(8):
|
||
+ for i in range(4 * len(eems)):
|
||
mosi = Signal()
|
||
setattr(self, "mosi{}".format(i), mosi)
|
||
- for i in range(4*len(eems)):
|
||
self.specials += [
|
||
- DifferentialOutput(getattr(self, "mosi{}".format(i)),
|
||
+ DifferentialOutput(mosi,
|
||
getattr(spip[i // 4], "mosi{}".format(i % 4)),
|
||
getattr(spin[i // 4], "mosi{}".format(i % 4)))
|
||
]
|
||
diff --git a/artiq/gateware/suservo/servo.py b/artiq/gateware/suservo/servo.py
|
||
index 1aec95f027..15d31027e0 100644
|
||
--- a/artiq/gateware/suservo/servo.py
|
||
+++ b/artiq/gateware/suservo/servo.py
|
||
@@ -42,7 +42,7 @@ def __init__(self, adc_pads, dds_pads, adc_p, iir_p, dds_p):
|
||
assert t_iir + 2*adc_p.channels < t_cycle, "need shifting time"
|
||
|
||
self.submodules.adc = ADC(adc_pads, adc_p)
|
||
- self.submodules.iir = IIR(iir_p)
|
||
+ self.submodules.iir = IIR(iir_p, adc_p, dds_p, t_cycle)
|
||
self.submodules.dds = DDS(dds_pads, dds_p)
|
||
|
||
# adc channels are reversed on Sampler
|
||
@@ -63,7 +63,6 @@ def __init__(self, adc_pads, dds_pads, adc_p, iir_p, dds_p):
|
||
assert t_restart > 1
|
||
cnt = Signal(max=t_restart)
|
||
cnt_done = Signal()
|
||
- active = Signal(3)
|
||
|
||
# Indicates whether different steps (0: ADC, 1: IIR, 2: DDS) are
|
||
# currently active (exposed for simulation only), with each bit being
|
||
@@ -71,6 +70,8 @@ def __init__(self, adc_pads, dds_pads, adc_p, iir_p, dds_p):
|
||
# timing details of the different steps, any number can be concurrently
|
||
# active (e.g. ADC read from iteration n, IIR computation from iteration
|
||
# n - 1, and DDS write from iteration n - 2).
|
||
+ active = Signal(3)
|
||
+ self._active = active # Exposed for debugging only.
|
||
|
||
# Asserted once per cycle when the DDS write has been completed.
|
||
self.done = Signal()
|
||
@@ -95,6 +96,17 @@ def __init__(self, adc_pads, dds_pads, adc_p, iir_p, dds_p):
|
||
cnt.eq(t_restart - 1)
|
||
)
|
||
]
|
||
+
|
||
+ # Count number of cycles since the servo was last started from idle.
|
||
+ self.sync += If(active == 0,
|
||
+ self.iir.t_running.eq(0),
|
||
+ self.iir.reset_dds_phase.eq(1)
|
||
+ ).Else(
|
||
+ self.iir.t_running.eq(self.iir.t_running + 1)
|
||
+ )
|
||
+
|
||
+ self.sync += dds_pads.passthrough.eq(active == 0)
|
||
+
|
||
self.comb += [
|
||
cnt_done.eq(cnt == 0),
|
||
self.adc.start.eq(self.start & cnt_done),
|
||
diff --git a/artiq/gateware/test/suservo/__init__.py b/artiq/gateware/test/suservo/__init__.py
|
||
index e69de29bb2..7a1df77ac1 100644
|
||
--- a/artiq/gateware/test/suservo/__init__.py
|
||
+++ b/artiq/gateware/test/suservo/__init__.py
|
||
@@ -0,0 +1,10 @@
|
||
+"""Gateware implementation of the Sampler-Urukul (AD9910) DDS amplitude servo.
|
||
+
|
||
+General conventions:
|
||
+
|
||
+ - ``t_...`` signals and constants refer to time spans measured in the gateware
|
||
+ module's default clock (typically a 125 MHz RTIO clock).
|
||
+ - ``start`` signals cause modules to proceed with the next servo iteration iff
|
||
+ they are currently idle (i.e. their value is irrelevant while the module is
|
||
+ busy, so they are not necessarily one-clock-period strobes).
|
||
+"""
|
||
diff --git a/artiq/gateware/test/suservo/test_dds.py b/artiq/gateware/test/suservo/test_dds.py
|
||
index a666f14c56..d9a8167590 100644
|
||
--- a/artiq/gateware/test/suservo/test_dds.py
|
||
+++ b/artiq/gateware/test/suservo/test_dds.py
|
||
@@ -5,6 +5,9 @@
|
||
|
||
from artiq.gateware.suservo.dds_ser import DDSParams, DDS
|
||
|
||
+class OutIoUpdateTB(Module):
|
||
+ def __init__(self):
|
||
+ self.fine_ts = Signal(3)
|
||
|
||
class TB(Module):
|
||
def __init__(self, p):
|
||
@@ -15,6 +18,12 @@ def __init__(self, p):
|
||
setattr(self, "mosi{}".format(i), m)
|
||
self.miso = Signal()
|
||
self.io_update = Signal()
|
||
+ self.passthrough = Signal()
|
||
+
|
||
+ self.io_update_phys = []
|
||
+ for i in range(p.channels//4):
|
||
+ phy = OutIoUpdateTB()
|
||
+ self.io_update_phys.append(phy)
|
||
|
||
clk0 = Signal()
|
||
self.sync += clk0.eq(self.clk)
|
||
@@ -23,16 +32,19 @@ def __init__(self, p):
|
||
|
||
self.ddss = []
|
||
for i in range(p.channels):
|
||
- dds = Record([("ftw", 32), ("pow", 16), ("asf", 16), ("cmd", 8)])
|
||
- sr = Signal(len(dds))
|
||
+ dds = Record([("ftw", 32), ("pow", 16), ("asf", 16),
|
||
+ ("cmd", 8), ("accu", 32), ("phase", 19)])
|
||
+ sr = Signal(32 + 16 + 16 + 8)
|
||
self.sync += [
|
||
+ dds.accu.eq(dds.accu + p.sysclk_per_clk * dds.ftw),
|
||
If(~self.cs_n & sample,
|
||
sr.eq(Cat(self.mosi[i], sr))
|
||
),
|
||
If(self.io_update,
|
||
- dds.raw_bits().eq(sr)
|
||
+ dds.raw_bits()[:len(sr)].eq(sr)
|
||
)
|
||
]
|
||
+ self.comb += dds.phase.eq((dds.pow << 3) + (dds.accu >> 13))
|
||
self.ddss.append(dds)
|
||
|
||
@passive
|
||
@@ -55,7 +67,7 @@ def log(self, data):
|
||
|
||
|
||
def main():
|
||
- p = DDSParams(channels=4, width=8 + 32 + 16 + 16, clk=1)
|
||
+ p = DDSParams(channels=4, width=8 + 32 + 16 + 16, clk=1, sysclk_per_clk=8)
|
||
tb = TB(p)
|
||
dds = DDS(tb, p)
|
||
tb.submodules += dds
|
||
diff --git a/artiq/gateware/test/suservo/test_iir.py b/artiq/gateware/test/suservo/test_iir.py
|
||
index 919e7a6bf9..ab8a9a4a46 100644
|
||
--- a/artiq/gateware/test/suservo/test_iir.py
|
||
+++ b/artiq/gateware/test/suservo/test_iir.py
|
||
@@ -2,48 +2,67 @@
|
||
import unittest
|
||
|
||
from migen import *
|
||
-from artiq.gateware.suservo import iir
|
||
+from artiq.gateware.suservo import servo
|
||
+from collections import namedtuple
|
||
|
||
+logger = logging.getLogger(__name__)
|
||
+
|
||
+ADCParamsSim = namedtuple("ADCParams", ["channels"])
|
||
+DDSParamsSim = namedtuple("ADCParams", ["channels", "sysclk_per_clk"])
|
||
|
||
def main():
|
||
- w_kasli = iir.IIRWidths(state=25, coeff=18, adc=16,
|
||
- asf=14, word=16, accu=48, shift=11,
|
||
- channel=3, profile=5, dly=8)
|
||
- w = iir.IIRWidths(state=17, coeff=16, adc=16,
|
||
- asf=14, word=16, accu=48, shift=11,
|
||
- channel=2, profile=1, dly=8)
|
||
+ w_kasli = servo.IIRWidths(state=25, coeff=18, adc=16, asf=14,
|
||
+ word=16, accu=48, shift=11, profile=5, dly=8)
|
||
+ p_adc = ADCParamsSim(channels=8)
|
||
+ p_dds = DDSParamsSim(channels=4, sysclk_per_clk=8)
|
||
+ w = servo.IIRWidths(state=17, coeff=16, adc=16, asf=14,
|
||
+ word=16, accu=48, shift=11, profile=2, dly=8)
|
||
|
||
+ t_iir = p_adc.channels + 4*p_dds.channels + 8 + 1
|
||
def run(dut):
|
||
+ yield dut.t_running.eq(0)
|
||
for i, ch in enumerate(dut.adc):
|
||
yield ch.eq(i)
|
||
for i, ch in enumerate(dut.ctrl):
|
||
yield ch.en_iir.eq(1)
|
||
yield ch.en_out.eq(1)
|
||
yield ch.profile.eq(i)
|
||
- for i in range(1 << w.channel):
|
||
+ yield ch.en_pt.eq(i)
|
||
+ for i, ch in enumerate(dut.ctrl_reftime):
|
||
+ yield ch.sysclks_fine.eq(i)
|
||
+ yield ch.stb.eq(1)
|
||
+ yield
|
||
+ yield dut.t_running.eq(dut.t_running + 1)
|
||
+ yield ch.stb.eq(0)
|
||
+ yield
|
||
+ yield dut.t_running.eq(dut.t_running + 1)
|
||
+ for i in range(p_adc.channels):
|
||
yield from dut.set_state(i, i << 8, coeff="x1")
|
||
yield from dut.set_state(i, i << 8, coeff="x0")
|
||
+ for i in range(p_dds.channels):
|
||
for j in range(1 << w.profile):
|
||
yield from dut.set_state(i,
|
||
(j << 1) | (i << 8), profile=j, coeff="y1")
|
||
for k, l in enumerate("pow offset ftw0 ftw1".split()):
|
||
yield from dut.set_coeff(i, profile=j, coeff=l,
|
||
- value=(i << 12) | (j << 8) | (k << 4))
|
||
+ value=(i << 10) | (j << 8) | (k << 4))
|
||
yield
|
||
- for i in range(1 << w.channel):
|
||
+ for i in range(p_dds.channels):
|
||
for j in range(1 << w.profile):
|
||
- for k, l in enumerate("cfg a1 b0 b1".split()):
|
||
+ for k, l in enumerate("a1 b0 b1".split()):
|
||
yield from dut.set_coeff(i, profile=j, coeff=l,
|
||
- value=(i << 12) | (j << 8) | (k << 4))
|
||
+ value=(i << 10) | (j << 8) | (k << 4))
|
||
yield from dut.set_coeff(i, profile=j, coeff="cfg",
|
||
- value=(i << 0) | (j << 8)) # sel, dly
|
||
+ value=(i % p_adc.channels) | (j << 8)) # sel, dly
|
||
yield
|
||
- for i in range(10):
|
||
+ for i in range(4):
|
||
+ logger.debug("check_iter {}".format(i))
|
||
yield from dut.check_iter()
|
||
+ yield dut.t_running.eq((yield dut.t_running) + t_iir)
|
||
yield
|
||
|
||
- dut = iir.IIR(w)
|
||
- run_simulation(dut, [run(dut)], vcd_name="iir.vcd")
|
||
+ dut = servo.IIR(w, p_adc, p_dds, t_iir)
|
||
+ run_simulation(dut, [run(dut)], vcd_name="servo.vcd")
|
||
|
||
|
||
class IIRTest(unittest.TestCase):
|
||
diff --git a/artiq/gateware/test/suservo/test_servo.py b/artiq/gateware/test/suservo/test_servo.py
|
||
index cc1a73a2be..fe1708d033 100644
|
||
--- a/artiq/gateware/test/suservo/test_servo.py
|
||
+++ b/artiq/gateware/test/suservo/test_servo.py
|
||
@@ -1,5 +1,6 @@
|
||
import logging
|
||
import unittest
|
||
+import numpy as np
|
||
|
||
from migen import *
|
||
from migen.genlib import io
|
||
@@ -7,15 +8,17 @@
|
||
from artiq.gateware.test.suservo import test_adc, test_dds
|
||
from artiq.gateware.suservo import servo
|
||
|
||
+logger = logging.getLogger(__name__)
|
||
+
|
||
|
||
class ServoSim(servo.Servo):
|
||
def __init__(self):
|
||
adc_p = servo.ADCParams(width=16, channels=8, lanes=4,
|
||
t_cnvh=4, t_conv=57 - 4, t_rtt=4 + 4)
|
||
iir_p = servo.IIRWidths(state=25, coeff=18, adc=16, asf=14, word=16,
|
||
- accu=48, shift=11, channel=3, profile=5, dly=8)
|
||
+ accu=48, shift=11, profile=5, dly=8)
|
||
dds_p = servo.DDSParams(width=8 + 32 + 16 + 16,
|
||
- channels=adc_p.channels, clk=1)
|
||
+ channels=4, clk=1, sysclk_per_clk=8)
|
||
|
||
self.submodules.adc_tb = test_adc.TB(adc_p)
|
||
self.submodules.dds_tb = test_dds.TB(dds_p)
|
||
@@ -23,37 +26,156 @@ def __init__(self):
|
||
servo.Servo.__init__(self, self.adc_tb, self.dds_tb,
|
||
adc_p, iir_p, dds_p)
|
||
|
||
+ self.dds_output = []
|
||
+
|
||
+ def log_flow(self, cycle):
|
||
+ su_start = yield self.start
|
||
+ adc_start = yield self.adc.start
|
||
+ iir_start = yield self.iir.start
|
||
+ dds_start = yield self.dds.start
|
||
+ su_done = yield self.done
|
||
+ adc_done = yield self.adc.done
|
||
+ iir_done = yield self.iir.done
|
||
+ dds_done = yield self.dds.done
|
||
+ active = yield self._active
|
||
+ io_update = yield self.dds_tb.io_update
|
||
+ passthrough = yield self.dds_tb.passthrough
|
||
+ iir_loading = yield self.iir.loading
|
||
+ iir_processing = yield self.iir.processing
|
||
+ iir_shifting = yield self.iir.shifting
|
||
+ dt = yield self.iir.t_running
|
||
+ dt_iir = yield self.iir._dt_start
|
||
+ state = yield self.iir._state
|
||
+ stage0 = yield self.iir._stages[0]
|
||
+ stage1 = yield self.iir._stages[1]
|
||
+ stage2 = yield self.iir._stages[2]
|
||
+ logger.debug(
|
||
+ "cycle=%d "
|
||
+ #"start=[su=%d adc=%d iir=%d dds=%d] "
|
||
+ #"done=[su=%d adc=%d iir=%d dds=%d] "
|
||
+ "active=%s load_proc_shft=%d%d%d stages_active=%d%d%d "
|
||
+ "io_update=%d passthrough=%d "
|
||
+ "dt=%d dt_iir=%d state=%d",
|
||
+ cycle,
|
||
+ #su_start, adc_start, iir_start, dds_start,
|
||
+ #su_done, adc_done, iir_done, dds_done,
|
||
+ '{:03b}'.format(active), iir_loading, iir_processing, iir_shifting, stage0, stage1, stage2,
|
||
+ io_update, passthrough,
|
||
+ dt, dt_iir//8, state
|
||
+ )
|
||
+
|
||
+ def log_state(self, channel, profile, calls=[0]):
|
||
+ calls[0] += 1
|
||
+ # if not (yield self._active[1]):
|
||
+ # return
|
||
+ yield from self.log_flow(calls[0] - 2)
|
||
+ return
|
||
+ cfg = yield from self.iir.get_coeff(channel, profile, "cfg")
|
||
+ sel = cfg & 0x7
|
||
+ x0 = yield from self.iir.get_state(sel, coeff="x0")
|
||
+ x1 = yield from self.iir.get_state(sel, coeff="x1")
|
||
+ y1 = yield from self.iir.get_state(channel, profile, coeff="y1")
|
||
+ _pow = yield from self.iir.get_coeff(channel, profile, "pow")
|
||
+ pow_iir = yield self.iir.dds[channel][2*self.iir.widths.word:3*self.iir.widths.word]
|
||
+ pow_dds = yield self.dds_tb.ddss[channel].pow
|
||
+ asf_dds = yield self.dds_tb.ddss[channel].asf
|
||
+ ftw_dds = yield self.dds_tb.ddss[channel].ftw
|
||
+ accu_dds = yield self.dds_tb.ddss[channel].accu
|
||
+ phase_dds = (yield self.dds_tb.ddss[channel].phase)
|
||
+ dds_output = np.cos(2*np.pi*phase_dds/2**19)
|
||
+ ph_coh = yield self.iir._ph_coh
|
||
+ ph_acc = yield self.iir._ph_acc
|
||
+ offset = yield from self.iir.get_coeff(channel, profile, "offset")
|
||
+ ftw0 = yield from self.iir.get_coeff(channel, profile, "ftw0")
|
||
+ ftw1 = yield from self.iir.get_coeff(channel, profile, "ftw1")
|
||
+ m_phase = yield from self.iir.get_accum_ftw(channel)
|
||
+ iir_adc = yield self.iir.adc[sel]
|
||
+ logger.debug("\t"
|
||
+ "ch=%d pr=%d "
|
||
+ # "x0=%d x1=%d adc=%d y1=%d sel=%d "
|
||
+ "ftw=%#x pow_coeff=%#x ftw_accu=%#x "
|
||
+ "ph_coh=%#x ph_acc=%#x "
|
||
+ "pow_iir=%#x pow_dds=%#x ftw_dds=%#x asf_dds=%#x accu_dds=%#x phase_dds=%#x dds_output=%04.3f",
|
||
+ channel, profile,
|
||
+ # x0, x1, iir_adc, y1, sel,
|
||
+ ftw0 | (ftw1 << 16), _pow, m_phase,
|
||
+ ph_coh, ph_acc,
|
||
+ pow_iir, pow_dds, ftw_dds, asf_dds, accu_dds, phase_dds >> 3, dds_output
|
||
+ )
|
||
+ self.dds_output.append(dds_output)
|
||
+ # yield from self.log_registers(profile)
|
||
+
|
||
+ def log_registers(self, profile):
|
||
+ adc_channels = self.iir.widths_adc.channels
|
||
+ dds_channels = self.iir.widths_dds.channels
|
||
+ x0s = [0]*adc_channels
|
||
+ x1s = [0]*adc_channels
|
||
+ y1s = [0]*dds_channels
|
||
+ for ch in range(adc_channels):
|
||
+ x0s[ch] = yield from self.iir.get_state(ch, coeff="x0")
|
||
+ x1s[ch] = yield from self.iir.get_state(ch, coeff="x1")
|
||
+ for ch in range(dds_channels):
|
||
+ y1s[ch] = yield from self.iir.get_state(ch, profile, coeff="y1")
|
||
+
|
||
+ logger.debug(("x0s = " + '{:05X} ' * adc_channels).format(*x0s))
|
||
+ logger.debug(("x1s = " + '{:05X} ' * adc_channels).format(*x1s))
|
||
+ logger.debug(("y1s = " + '{:05X} ' * dds_channels).format(*y1s))
|
||
+
|
||
def test(self):
|
||
assert (yield self.done)
|
||
|
||
- adc = 1
|
||
+ adc = 7
|
||
x0 = 0x0141
|
||
yield self.adc_tb.data[-adc-1].eq(x0)
|
||
- channel = 3
|
||
- yield self.iir.adc[channel].eq(adc)
|
||
+ channel = 0
|
||
yield self.iir.ctrl[channel].en_iir.eq(1)
|
||
yield self.iir.ctrl[channel].en_out.eq(1)
|
||
- profile = 5
|
||
+ yield self.iir.ctrl[channel].en_pt.eq(1)
|
||
+ profile = 31
|
||
yield self.iir.ctrl[channel].profile.eq(profile)
|
||
x1 = 0x0743
|
||
yield from self.iir.set_state(adc, x1, coeff="x1")
|
||
y1 = 0x1145
|
||
yield from self.iir.set_state(channel, y1,
|
||
profile=profile, coeff="y1")
|
||
- coeff = dict(pow=0x1333, offset=0x1531, ftw0=0x1727, ftw1=0x1929,
|
||
- a1=0x0135, b0=0x0337, b1=0x0539, cfg=adc | (0 << 3))
|
||
+ coeff = dict(pow=0, offset=0x1531, ftw0=0xeb85, ftw1=0x51,
|
||
+ a1=0x0135, b0=0x0337, b1=0x0539, cfg=adc)
|
||
for ks in "pow offset ftw0 ftw1", "a1 b0 b1 cfg":
|
||
for k in ks.split():
|
||
yield from self.iir.set_coeff(channel, value=coeff[k],
|
||
profile=profile, coeff=k)
|
||
yield
|
||
|
||
+ num_it = 1
|
||
+ num_proc_its = [0]*num_it # number of iterations while iir.processing
|
||
+ yield from self.log_state(channel, profile)
|
||
yield self.start.eq(1)
|
||
yield
|
||
- yield self.start.eq(0)
|
||
- while not (yield self.dds_tb.io_update):
|
||
- yield
|
||
- yield # io_update
|
||
+ for i in range(num_it):
|
||
+ if i == 1: # change ftw
|
||
+ yield from self.iir.set_coeff(channel,
|
||
+ profile=profile, coeff='ftw0', value=coeff['ftw1'])
|
||
+ yield from self.iir.set_coeff(channel,
|
||
+ profile=profile, coeff='ftw1', value=coeff['ftw0'])
|
||
+ if i == 2: # change ftw back
|
||
+ yield from self.iir.set_coeff(channel,
|
||
+ profile=profile, coeff='ftw0', value=coeff['ftw0'])
|
||
+ yield from self.iir.set_coeff(channel,
|
||
+ profile=profile, coeff='ftw1', value=coeff['ftw1'])
|
||
+ logger.debug("iteration {}".format(i))
|
||
+ yield from self.log_state(channel, profile)
|
||
+ if i == num_it-1:
|
||
+ yield self.start.eq(0)
|
||
+ while not (yield self.dds_tb.io_update):
|
||
+ yield
|
||
+ if (yield self.iir.processing):
|
||
+ num_proc_its[i] += 1
|
||
+ if (yield self.iir._stages) != 0:
|
||
+ yield from self.log_state(channel, profile)
|
||
+ yield # io_update
|
||
+ yield from self.log_state(channel, profile)
|
||
+ yield
|
||
+ yield from self.log_state(channel, profile)
|
||
|
||
w = self.iir.widths
|
||
|
||
@@ -63,6 +185,8 @@ def test(self):
|
||
|
||
offset = coeff["offset"] << (w.state - w.coeff - 1)
|
||
a1, b0, b1 = coeff["a1"], coeff["b0"], coeff["b1"]
|
||
+
|
||
+ # works only for 1 iteration
|
||
out = (
|
||
0*(1 << w.shift - 1) + # rounding
|
||
a1*(y1 + 0) + b0*(x0 + offset) + b1*(x1 + offset)
|
||
@@ -76,8 +200,15 @@ def test(self):
|
||
ftw = (coeff["ftw1"] << 16) | coeff["ftw0"]
|
||
assert _ == ftw, (hex(_), hex(ftw))
|
||
|
||
+ t0 = yield self.iir._dt_start
|
||
+ # todo: include phase accumulator
|
||
+ ph = (ftw * t0) >> 16
|
||
+ if (yield self.iir.ctrl[channel].en_pt):
|
||
+ pow = (coeff["pow"] + ph) & 0xffff
|
||
+ else:
|
||
+ pow = coeff["pow"]
|
||
_ = yield self.dds_tb.ddss[channel].pow
|
||
- assert _ == coeff["pow"], (hex(_), hex(coeff["pow"]))
|
||
+ assert _ == pow, (hex(_), hex(pow))
|
||
|
||
_ = yield self.dds_tb.ddss[channel].asf
|
||
asf = y1 >> (w.state - w.asf - 1)
|
||
@@ -101,4 +232,5 @@ def test_run(self):
|
||
|
||
|
||
if __name__ == "__main__":
|
||
+ logging.basicConfig(level=logging.DEBUG)
|
||
main()
|