forked from M-Labs/artiq
1
0
Fork 0

AD9910 driver feature extension and SUServo IIR readability (#1500)

* coredevice.ad9910: Add set_cfr2 function and extend arguments of set_cfr1 and set_sync

* SUServo: Wrap CPLD and DDS devices in a list

* SUServo: Refactor [nfc]

Co-authored-by: drmota <peter.drmota@physics.ox.ac.uk>
Co-authored-by: David Nadlinger <code@klickverbot.at>
This commit is contained in:
Peter Drmota 2021-11-15 05:09:16 +01:00 committed by GitHub
parent f0c50c80e6
commit 20e079a381
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 301 additions and 179 deletions

View File

@ -104,6 +104,9 @@ Breaking changes:
* ``quamash`` has been replaced with ``qasync``.
* Protocols are updated to use device endian.
* Analyzer dump format includes a byte for device endianness.
* To support variable numbers of Urukul cards in the future, the
``artiq.coredevice.suservo.SUServo`` constructor now accepts two device name lists,
``cpld_devices`` and ``dds_devices``, rather than four individual arguments.
* Experiment classes with underscore-prefixed names are now ignored when ``artiq_client``
determines which experiment to submit (consistent with ``artiq_run``).

View File

@ -374,18 +374,25 @@ class AD9910:
data[(n - preload) + i] = self.bus.read()
@kernel
def set_cfr1(self, power_down: TInt32 = 0b0000,
def set_cfr1(self,
power_down: TInt32 = 0b0000,
phase_autoclear: TInt32 = 0,
drg_load_lrr: TInt32 = 0, drg_autoclear: TInt32 = 0,
internal_profile: TInt32 = 0, ram_destination: TInt32 = 0,
ram_enable: TInt32 = 0, manual_osk_external: TInt32 = 0,
osk_enable: TInt32 = 0, select_auto_osk: TInt32 = 0):
drg_load_lrr: TInt32 = 0,
drg_autoclear: TInt32 = 0,
phase_clear: TInt32 = 0,
internal_profile: TInt32 = 0,
ram_destination: TInt32 = 0,
ram_enable: TInt32 = 0,
manual_osk_external: TInt32 = 0,
osk_enable: TInt32 = 0,
select_auto_osk: TInt32 = 0):
"""Set CFR1. See the AD9910 datasheet for parameter meanings.
This method does not pulse IO_UPDATE.
:param power_down: Power down bits.
:param phase_autoclear: Autoclear phase accumulator.
:param phase_clear: Asynchronous, static reset of the phase accumulator.
:param drg_load_lrr: Load digital ramp generator LRR.
:param drg_autoclear: Autoclear digital ramp generator.
:param internal_profile: Internal profile control.
@ -405,11 +412,41 @@ class AD9910:
(drg_load_lrr << 15) |
(drg_autoclear << 14) |
(phase_autoclear << 13) |
(phase_clear << 11) |
(osk_enable << 9) |
(select_auto_osk << 8) |
(power_down << 4) |
2) # SDIO input only, MSB first
@kernel
def set_cfr2(self,
asf_profile_enable: TInt32 = 1,
drg_enable: TInt32 = 0,
effective_ftw: TInt32 = 1,
sync_validation_disable: TInt32 = 0,
matched_latency_enable: TInt32 = 0):
"""Set CFR2. See the AD9910 datasheet for parameter meanings.
This method does not pulse IO_UPDATE.
:param asf_profile_enable: Enable amplitude scale from single tone profiles.
:param drg_enable: Digital ramp enable.
:param effective_ftw: Read effective FTW.
:param sync_validation_disable: Disable the SYNC_SMP_ERR pin indicating
(active high) detection of a synchronization pulse sampling error.
:param matched_latency_enable: Simultaneous application of amplitude,
phase, and frequency changes to the DDS arrive at the output
* matched_latency_enable = 0: in the order listed
* matched_latency_enable = 1: simultaneously.
"""
self.write32(_AD9910_REG_CFR2,
(asf_profile_enable << 24) |
(drg_enable << 19) |
(effective_ftw << 16) |
(matched_latency_enable << 7) |
(sync_validation_disable << 5))
@kernel
def init(self, blind: TBool = False):
"""Initialize and configure the DDS.
@ -442,7 +479,7 @@ class AD9910:
# enable amplitude scale from profiles
# read effective FTW
# sync timing validation disable (enabled later)
self.write32(_AD9910_REG_CFR2, 0x01010020)
self.set_cfr2(sync_validation_disable=1)
self.cpld.io_update.pulse(1 * us)
cfr3 = (0x0807c000 | (self.pll_vco << 24) |
(self.pll_cp << 19) | (self.pll_en << 8) |
@ -465,7 +502,7 @@ class AD9910:
if i >= 100 - 1:
raise ValueError("PLL lock timeout")
delay(10 * us) # slack
if self.sync_data.sync_delay_seed >= 0:
if self.sync_data.sync_delay_seed >= 0 and not blind:
self.tune_sync_delay(self.sync_data.sync_delay_seed)
delay(1 * ms)
@ -875,20 +912,26 @@ class AD9910:
self.cpld.cfg_sw(self.chip_select - 4, state)
@kernel
def set_sync(self, in_delay: TInt32, window: TInt32):
def set_sync(self,
in_delay: TInt32,
window: TInt32,
en_sync_gen: TInt32 = 0):
"""Set the relevant parameters in the multi device synchronization
register. See the AD9910 datasheet for details. The SYNC clock
generator preset value is set to zero, and the SYNC_OUT generator is
disabled.
disabled by default.
:param in_delay: SYNC_IN delay tap (0-31) in steps of ~75ps
:param window: Symmetric SYNC_IN validation window (0-15) in
steps of ~75ps for both hold and setup margin.
:param en_sync_gen: Whether to enable the DDS-internal sync generator
(SYNC_OUT, cf. sync_sel == 1). Should be left off for the normal
use case, where the SYNC clock is supplied by the core device.
"""
self.write32(_AD9910_REG_SYNC,
(window << 28) | # SYNC S/H validation delay
(1 << 27) | # SYNC receiver enable
(0 << 26) | # SYNC generator disable
(en_sync_gen << 26) | # SYNC generator enable
(0 << 25) | # SYNC generator SYS rising edge
(0 << 18) | # SYNC preset
(0 << 11) | # SYNC output delay
@ -904,9 +947,10 @@ class AD9910:
Also modifies CFR2.
"""
self.write32(_AD9910_REG_CFR2, 0x01010020) # clear SMP_ERR
self.set_cfr2(sync_validation_disable=1) # clear SMP_ERR
self.cpld.io_update.pulse(1 * us)
self.write32(_AD9910_REG_CFR2, 0x01010000) # enable SMP_ERR
delay(10 * us) # slack
self.set_cfr2(sync_validation_disable=0) # enable SMP_ERR
self.cpld.io_update.pulse(1 * us)
@kernel
@ -984,7 +1028,7 @@ class AD9910:
# set up DRG
self.set_cfr1(drg_load_lrr=1, drg_autoclear=1)
# DRG -> FTW, DRG enable
self.write32(_AD9910_REG_CFR2, 0x01090000)
self.set_cfr2(drg_enable=1)
# no limits
self.write64(_AD9910_REG_RAMP_LIMIT, -1, 0)
# DRCTL=0, dt=1 t_SYNC_CLK
@ -1005,7 +1049,7 @@ class AD9910:
ftw = self.read32(_AD9910_REG_FTW) # read out effective FTW
delay(100 * us) # slack
# disable DRG
self.write32(_AD9910_REG_CFR2, 0x01010000)
self.set_cfr2(drg_enable=0)
self.cpld.io_update.pulse_mu(8)
return ftw & 1

View File

@ -57,32 +57,26 @@ class SUServo:
:param channel: RTIO channel number
:param pgia_device: Name of the Sampler PGIA gain setting SPI bus
:param cpld0_device: Name of the first Urukul CPLD SPI bus
:param cpld1_device: Name of the second Urukul CPLD SPI bus
:param dds0_device: Name of the AD9910 device for the DDS on the first
Urukul
:param dds1_device: Name of the AD9910 device for the DDS on the second
Urukul
:param cpld_devices: Names of the Urukul CPLD SPI buses
:param dds_devices: Names of the AD9910 devices
:param gains: Initial value for PGIA gains shift register
(default: 0x0000). Knowledge of this state is not transferred
between experiments.
:param core_device: Core device name
"""
kernel_invariants = {"channel", "core", "pgia", "cpld0", "cpld1",
"dds0", "dds1", "ref_period_mu"}
kernel_invariants = {"channel", "core", "pgia", "cplds", "ddses",
"ref_period_mu"}
def __init__(self, dmgr, channel, pgia_device,
cpld0_device, cpld1_device,
dds0_device, dds1_device,
cpld_devices, dds_devices,
gains=0x0000, core_device="core"):
self.core = dmgr.get(core_device)
self.pgia = dmgr.get(pgia_device)
self.pgia.update_xfer_duration_mu(div=4, length=16)
self.dds0 = dmgr.get(dds0_device)
self.dds1 = dmgr.get(dds1_device)
self.cpld0 = dmgr.get(cpld0_device)
self.cpld1 = dmgr.get(cpld1_device)
assert len(dds_devices) == len(cpld_devices)
self.ddses = [dmgr.get(dds) for dds in dds_devices]
self.cplds = [dmgr.get(cpld) for cpld in cpld_devices]
self.channel = channel
self.gains = gains
self.ref_period_mu = self.core.seconds_to_mu(
@ -109,17 +103,15 @@ class SUServo:
sampler.SPI_CONFIG | spi.SPI_END,
16, 4, sampler.SPI_CS_PGIA)
self.cpld0.init(blind=True)
cfg0 = self.cpld0.cfg_reg
self.cpld0.cfg_write(cfg0 | (0xf << urukul.CFG_MASK_NU))
self.dds0.init(blind=True)
self.cpld0.cfg_write(cfg0)
for i in range(len(self.cplds)):
cpld = self.cplds[i]
dds = self.ddses[i]
self.cpld1.init(blind=True)
cfg1 = self.cpld1.cfg_reg
self.cpld1.cfg_write(cfg1 | (0xf << urukul.CFG_MASK_NU))
self.dds1.init(blind=True)
self.cpld1.cfg_write(cfg1)
cpld.init(blind=True)
prev_cpld_cfg = cpld.cfg_reg
cpld.cfg_write(prev_cpld_cfg | (0xf << urukul.CFG_MASK_NU))
dds.init(blind=True)
cpld.cfg_write(prev_cpld_cfg)
@kernel
def write(self, addr, value):
@ -257,9 +249,11 @@ class Channel:
self.servo = dmgr.get(servo_device)
self.core = self.servo.core
self.channel = channel
# FIXME: this assumes the mem channel is right after the control
# channels
self.servo_channel = self.channel + 8 - self.servo.channel
# This assumes the mem channel is right after the control channels
# Make sure this is always the case in eem.py
self.servo_channel = (self.channel + 4 * len(self.servo.cplds) -
self.servo.channel)
self.dds = self.servo.ddses[self.servo_channel // 4]
@kernel
def set(self, en_out, en_iir=0, profile=0):
@ -311,12 +305,8 @@ class Channel:
see :meth:`dds_offset_to_mu`
:param phase: DDS phase in turns
"""
if self.servo_channel < 4:
dds = self.servo.dds0
else:
dds = self.servo.dds1
ftw = dds.frequency_to_ftw(frequency)
pow_ = dds.turns_to_pow(phase)
ftw = self.dds.frequency_to_ftw(frequency)
pow_ = self.dds.turns_to_pow(phase)
offs = self.dds_offset_to_mu(offset)
self.set_dds_mu(profile, ftw, offs, pow_)

View File

@ -191,10 +191,8 @@ device_db = {
"arguments": {
"channel": 24,
"pgia_device": "spi_sampler0_pgia",
"cpld0_device": "urukul0_cpld",
"cpld1_device": "urukul1_cpld",
"dds0_device": "urukul0_dds",
"dds1_device": "urukul1_dds"
"cpld_devices": ["urukul0_cpld", "urukul1_cpld"],
"dds_devices": ["urukul0_dds", "urukul1_dds"],
}
},

View File

@ -364,8 +364,7 @@ class PeripheralManager:
def process_suservo(self, rtio_offset, peripheral):
suservo_name = self.get_name("suservo")
sampler_name = self.get_name("sampler")
urukul0_name = self.get_name("urukul")
urukul1_name = self.get_name("urukul")
urukul_names = [self.get_name("urukul") for _ in range(2)]
channel = count(0)
for i in range(8):
self.gen("""
@ -386,16 +385,14 @@ class PeripheralManager:
"arguments": {{
"channel": 0x{suservo_channel:06x},
"pgia_device": "spi_{sampler_name}_pgia",
"cpld0_device": "{urukul0_name}_cpld",
"cpld1_device": "{urukul1_name}_cpld",
"dds0_device": "{urukul0_name}_dds",
"dds1_device": "{urukul1_name}_dds"
"cpld_devices": {cpld_names_list},
"dds_devices": {dds_names_list}
}}
}}""",
suservo_name=suservo_name,
sampler_name=sampler_name,
urukul0_name=urukul0_name,
urukul1_name=urukul1_name,
cpld_names_list=[urukul_name + "_cpld" for urukul_name in urukul_names],
dds_names_list=[urukul_name + "_dds" for urukul_name in urukul_names],
suservo_channel=rtio_offset+next(channel))
self.gen("""
device_db["spi_{sampler_name}_pgia"] = {{
@ -407,7 +404,7 @@ class PeripheralManager:
sampler_name=sampler_name,
sampler_channel=rtio_offset+next(channel))
pll_vco = peripheral.get("pll_vco")
for urukul_name in (urukul0_name, urukul1_name):
for urukul_name in urukul_names:
self.gen("""
device_db["spi_{urukul_name}"] = {{
"type": "local",

View File

@ -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).
"""

View File

@ -1,9 +1,7 @@
from collections import namedtuple
import logging
from migen import *
logger = logging.getLogger(__name__)
@ -222,14 +220,13 @@ class IIR(Module):
assert w.word <= w.coeff # same memory
assert w.state + w.coeff + 3 <= w.accu
# m_coeff of active profiles should only be accessed during
# 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)
# m_state[x] should only be read during ~(shifting |
# loading)
# m_state[y] of active profiles should only be read during
# 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
@ -246,7 +243,7 @@ class IIR(Module):
self.adc = [Signal((w.adc, True), reset_less=True)
for i in range(1 << w.channel)]
# Cat(ftw0, ftw1, pow, asf)
# only read during ~processing
# only read externally during ~processing
self.dds = [Signal(4*w.word, reset_less=True)
for i in range(1 << w.channel)]
# perform one IIR iteration, start with loading,
@ -270,100 +267,116 @@ class IIR(Module):
en_iirs = Array([ch.en_iir for ch in self.ctrl])
clips = Array([ch.clip for ch in self.ctrl])
# state counter
state = Signal(w.channel + 2)
# pipeline group activity flags (SR)
stage = Signal(3)
# 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
# above). The steps share the same memory ports, and are executed
# strictly sequentially.
#
# LOAD/SHIFT just read/write one address per cycle; the duration needed
# to iterate over all channels is determined by counting cycles.
#
# The PROCESSing step is split across a three-stage pipeline, where each
# stage has up to four clock cycles latency. We feed the first stage
# 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")
state_clr = Signal()
stage_en = Signal()
t_current_step = Signal(w.channel + 2)
t_current_step_clr = Signal()
# pipeline group activity flags (SR)
# 0: load from memory
# 1: compute
# 2: write to output registers (DDS profiles, clip flags)
stages_active = Signal(3)
fsm.act("IDLE",
self.done.eq(1),
state_clr.eq(1),
t_current_step_clr.eq(1),
If(self.start,
NextState("LOAD")
)
)
fsm.act("LOAD",
self.loading.eq(1),
If(state == (1 << w.channel) - 1,
state_clr.eq(1),
stage_en.eq(1),
If(t_current_step == (1 << w.channel) - 1,
t_current_step_clr.eq(1),
NextValue(stages_active[0], 1),
NextState("PROCESS")
)
)
fsm.act("PROCESS",
self.processing.eq(1),
# this is technically wasting three cycles
# (one for setting stage, and phase=2,3 with stage[2])
If(stage == 0,
state_clr.eq(1),
NextState("SHIFT")
# (one for setting stages_active, and phase=2,3 with stages_active[2])
If(stages_active == 0,
t_current_step_clr.eq(1),
NextState("SHIFT"),
)
)
fsm.act("SHIFT",
self.shifting.eq(1),
If(state == (2 << w.channel) - 1,
If(t_current_step == (2 << w.channel) - 1,
NextState("IDLE")
)
)
self.sync += [
state.eq(state + 1),
If(state_clr,
state.eq(0),
),
If(stage_en,
stage[0].eq(1)
If(t_current_step_clr,
t_current_step.eq(0)
).Else(
t_current_step.eq(t_current_step + 1)
)
]
# pipeline group channel pointer
# global pipeline phase (lower two bits of t_current_step)
pipeline_phase = Signal(2, reset_less=True)
# 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)]
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,
stages_active[0].eq(0)
)
)
]
# pipeline group profile pointer (SR)
# for each pipeline stage, this is the profile currently being
# processed
profile = [Signal(w.profile, reset_less=True) for i in range(2)]
# pipeline phase (lower two bits of state)
phase = Signal(2, reset_less=True)
self.comb += Cat(phase, channel[0]).eq(state)
self.sync += [
Case(phase, {
0: [
If(pipeline_phase == 0,
profile[0].eq(profiles[channel[0]]),
profile[1].eq(profile[0])
],
3: [
Cat(channel[1:]).eq(Cat(channel[:-1])),
stage[1:].eq(stage[:-1]),
If(channel[0] == (1 << w.channel) - 1,
stage[0].eq(0)
profile[1].eq(profile[0]),
)
]
})
]
m_coeff = self.m_coeff.get_port()
m_state = self.m_state.get_port(write_capable=True) # mode=READ_FIRST
self.specials += m_state, m_coeff
#
# Hook up main IIR filter.
#
dsp = DSP(w)
self.submodules += dsp
offset_clr = Signal()
self.comb += [
m_coeff.adr.eq(Cat(phase, profile[0],
Mux(phase==0, channel[1], channel[0]))),
m_coeff.adr.eq(Cat(pipeline_phase, profile[0],
Mux(pipeline_phase == 0, channel[1], channel[0]))),
dsp.offset[-w.coeff - 1:].eq(Mux(offset_clr, 0,
Cat(m_coeff.dat_r[:w.coeff], m_coeff.dat_r[w.coeff - 1])
)),
dsp.coeff.eq(m_coeff.dat_r[w.coeff:]),
dsp.state.eq(m_state.dat_r),
Case(phase, {
Case(pipeline_phase, {
0: dsp.accu_clr.eq(1),
2: [
offset_clr.eq(1),
@ -373,6 +386,11 @@ class IIR(Module):
})
]
#
# Arbitrate state memory access between steps.
#
# 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)
@ -389,13 +407,13 @@ class IIR(Module):
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(state | (1 << w.profile + w.channel)),
m_state.adr.eq(t_current_step | (1 << w.profile + w.channel)),
m_state.dat_w.eq(m_state.dat_r),
m_state.we.eq(state[0])
m_state.we.eq(t_current_step[0])
),
If(self.loading,
m_state.adr.eq((state << 1) | (1 << w.profile + w.channel)),
m_state.dat_w[-w.adc - 1:-1].eq(Array(self.adc)[state]),
m_state.adr.eq((t_current_step << 1) | (1 << w.profile + w.channel)),
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)
),
@ -405,16 +423,20 @@ class IIR(Module):
Cat(profile[1], channel[2]),
# read old y
Cat(profile[0], channel[0]),
# x0 (recent)
# read x0 (recent)
0 | (sel_profile << 1) | (1 << w.profile + w.channel),
# x1 (old)
# read x1 (old)
1 | (sel << 1) | (1 << w.profile + w.channel),
])[phase]),
])[pipeline_phase]),
m_state.dat_w.eq(dsp.output),
m_state.we.eq((phase == 0) & stage[2] & en[1]),
m_state.we.eq((pipeline_phase == 0) & stages_active[2] & en[1]),
)
]
#
# Compute auxiliary signals (delayed servo enable, clip indicators, etc.).
#
# internal channel delay counters
dlys = Array([Signal(w.dly)
for i in range(1 << w.channel)])
@ -434,48 +456,62 @@ class IIR(Module):
en_out = Signal(reset_less=True)
# latched channel en_iir
en_iir = Signal(reset_less=True)
# muxing
ddss = Array(self.dds)
self.sync += [
Case(phase, {
Case(pipeline_phase, {
0: [
dly.eq(dlys[channel[0]]),
en_out.eq(en_outs[channel[0]]),
en_iir.eq(en_iirs[channel[0]]),
If(stage[1],
ddss[channel[1]][:w.word].eq(m_coeff.dat_r)
),
If(stage[2] & en[1] & dsp.clip,
If(stages_active[2] & en[1] & dsp.clip,
clips[channel[2]].eq(1)
)
],
1: [
If(stage[1],
ddss[channel[1]][w.word:2*w.word].eq(
m_coeff.dat_r),
),
If(stage[2],
ddss[channel[2]][3*w.word:].eq(
m_state.dat_r[w.state - w.asf - 1:w.state - 1])
)
],
2: [
en[0].eq(0),
en[1].eq(en[0]),
sel.eq(sel_profile),
If(stage[0],
ddss[channel[0]][2*w.word:3*w.word].eq(
m_coeff.dat_r),
If(en_out,
If(stages_active[0] & en_out,
If(dly != dly_profile,
dlys[channel[0]].eq(dly + 1)
).Elif(en_iir,
en[0].eq(1)
)
)
],
}),
]
#
# 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.
#
# muxing
ddss = Array(self.dds)
self.sync += [
Case(pipeline_phase, {
0: [
If(stages_active[1],
ddss[channel[1]][:w.word].eq(m_coeff.dat_r), # ftw0
),
],
1: [
If(stages_active[1],
ddss[channel[1]][w.word:2 * w.word].eq(m_coeff.dat_r), # ftw1
),
If(stages_active[2],
ddss[channel[2]][3*w.word:].eq( # asf
m_state.dat_r[w.state - w.asf - 1:w.state - 1])
)
],
2: [
If(stages_active[0],
ddss[channel[0]][2*w.word:3*w.word].eq(m_coeff.dat_r), # pow
),
],
3: [
],
}),

View File

@ -5,32 +5,76 @@ from .iir import IIR, IIRWidths
from .dds_ser import DDS, DDSParams
def predict_timing(adc_p, iir_p, dds_p):
"""
The following is a sketch of the timing for 1 Sampler (8 ADCs) and N Urukuls
Shown here, the cycle duration is limited by the IIR loading+processing time.
ADC|CONVH|CONV|READ|RTT|IDLE|CONVH|CONV|READ|RTT|IDLE|CONVH|CONV|READ|RTT|...
|4 |57 |16 |8 | .. |4 |57 |16 |8 | .. |4 |57 |16 |8 |...
---+-------------------+------------------------+------------------------+---
IIR| |LOAD|PROC |SHIFT|LOAD|PROC |SHIFT|...
| |8 |16*N+9 |16 |8 |16*N+9 |16 |...
---+--------------------------------------+------------------------+---------
DDS| |CMD|PROF|WAIT|IO_UP|IDLE|CMD|PR...
| |16 |128 |1 |1 | .. |16 | ...
IIR loading starts once the ADC presents its data, the DDSes are updated
once the IIR processing is over. These are the only blocking processes.
IIR shifting happens in parallel to writing to the DDSes and ADC conversions
take place while the IIR filter is processing or the DDSes are being
written to, depending on the cycle duration (given by whichever module
takes the longest).
"""
t_adc = (adc_p.t_cnvh + adc_p.t_conv + adc_p.t_rtt +
adc_p.channels*adc_p.width//adc_p.lanes) + 1
# load adc_p.channels values, process dds_p.channels
# (4 processing phases and 2 additional stages à 4 phases
# to complete the processing of the last channel)
t_iir = adc_p.channels + 4*dds_p.channels + 8 + 1
t_dds = (dds_p.width*2 + 1)*dds_p.clk + 1
t_cycle = max(t_adc, t_iir, t_dds)
return t_adc, t_iir, t_dds, t_cycle
class Servo(Module):
def __init__(self, adc_pads, dds_pads, adc_p, iir_p, dds_p):
t_adc, t_iir, t_dds, t_cycle = predict_timing(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.dds = DDS(dds_pads, dds_p)
# adc channels are reversed on Sampler
for i, j, k, l in zip(reversed(self.adc.data), self.iir.adc,
self.iir.dds, self.dds.profile):
self.comb += j.eq(i), l.eq(k)
t_adc = (adc_p.t_cnvh + adc_p.t_conv + adc_p.t_rtt +
adc_p.channels*adc_p.width//adc_p.lanes) + 1
t_iir = ((1 + 4 + 1) << iir_p.channel) + 1
t_dds = (dds_p.width*2 + 1)*dds_p.clk + 1
t_cycle = max(t_adc, t_iir, t_dds)
assert t_iir + (2 << iir_p.channel) < t_cycle, "need shifting time"
for iir, adc in zip(self.iir.adc, reversed(self.adc.data)):
self.comb += iir.eq(adc)
for dds, iir in zip(self.dds.profile, self.iir.dds):
self.comb += dds.eq(iir)
# If high, a new cycle is started if the current cycle (if any) is
# finished. Consequently, if low, servo iterations cease after the
# current cycle is finished. Don't care while the first step (ADC)
# is active.
self.start = Signal()
# Counter for delay between end of ADC cycle and start of next one,
# depending on the duration of the other steps.
t_restart = t_cycle - t_adc + 1
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
# reset once the successor step is launched. Depending on the
# 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).
# Asserted once per cycle when the DDS write has been completed.
self.done = Signal()
self.sync += [
If(self.dds.done,
active[2].eq(0)

View File

@ -228,9 +228,9 @@ class SUServo(StandaloneBase):
ttl_serdes_7series.Output_8X, ttl_serdes_7series.Output_8X)
# EEM3/2: Sampler, EEM5/4: Urukul, EEM7/6: Urukul
eem.SUServo.add_std(
self, eems_sampler=(3, 2),
eems_urukul0=(5, 4), eems_urukul1=(7, 6))
eem.SUServo.add_std(self,
eems_sampler=(3, 2),
eems_urukul=[[5, 4], [7, 6]])
for i in (1, 2):
sfp_ctl = self.platform.request("sfp_ctl", i)