ad9910: add phase modes

* simplified and cross-referenced the explanation of the different
  phase modes.
* semantically and functionally merged absolute and tracking/coherent
  phase modes.
* simplified numerics to calculate phase correction
* added warning about possible inconsistency with DMA and default
  phase mode
* restricted __all__ imports
* moved continuous/relative phase offset tracking from an instance
  variable to a "handle" returned by set()/set_mu() in order to avoid
  state inconsistency with DMA (#1113 #1115)

for #1143

Signed-off-by: Robert Jördens <rj@quartiq.de>
This commit is contained in:
Robert Jördens 2018-11-02 14:55:56 +00:00
parent d3ad2b7633
commit 2f6d3f79ff

View File

@ -1,14 +1,27 @@
from numpy import int32, int64
from artiq.language.core import kernel, delay, portable
from artiq.language.core import (
kernel, delay, portable, delay_mu, now_mu, at_mu)
from artiq.language.units import us, ns, ms
from artiq.coredevice import spi2 as spi
from artiq.coredevice import urukul
# Work around ARTIQ-Python import machinery
urukul_sta_pll_lock = urukul.urukul_sta_pll_lock
urukul_sta_smp_err = urukul.urukul_sta_smp_err
__all__ = [
"AD9910",
"PHASE_MODE_CONTINUOUS", "PHASE_MODE_ABSOLUTE", "PHASE_MODE_TRACKING"
]
_PHASE_MODE_DEFAULT = -1
PHASE_MODE_CONTINUOUS = 0
PHASE_MODE_ABSOLUTE = 1
PHASE_MODE_TRACKING = 2
_AD9910_REG_CFR1 = 0x00
_AD9910_REG_CFR2 = 0x01
_AD9910_REG_CFR3 = 0x02
@ -59,7 +72,7 @@ class AD9910:
set this to the delay tap number returned.
"""
kernel_invariants = {"chip_select", "cpld", "core", "bus",
"ftw_per_hz", "pll_n", "io_update_delay"}
"ftw_per_hz", "pll_n", "io_update_delay", "sysclk_per_mu"}
def __init__(self, dmgr, chip_select, cpld_device, sw_device=None,
pll_n=40, pll_cp=7, pll_vco=5, sync_delay_seed=-1,
@ -77,7 +90,9 @@ class AD9910:
assert self.cpld.refclk/4 <= 60e6
sysclk = self.cpld.refclk*pll_n/4 # Urukul clock fanout divider
assert sysclk <= 1e9
self.ftw_per_hz = 1./sysclk*(int64(1) << 32)
self.ftw_per_hz = (1 << 32)/sysclk
self.sysclk_per_mu = int(round(sysclk*self.core.ref_period))
assert self.sysclk_per_mu == sysclk*self.core.ref_period
assert 0 <= pll_vco <= 5
vco_min, vco_max = [(370, 510), (420, 590), (500, 700),
(600, 880), (700, 950), (820, 1150)][pll_vco]
@ -87,6 +102,49 @@ class AD9910:
self.pll_cp = pll_cp
self.sync_delay_seed = sync_delay_seed
self.io_update_delay = io_update_delay
self.phase_mode = PHASE_MODE_CONTINUOUS
@kernel
def set_phase_mode(self, phase_mode):
"""Sets the default phase mode for future calls to :meth:`set` and
:meth:`set_mu`. Supported phase modes are:
* :const:`PHASE_MODE_CONTINUOUS`: the phase accumulator is unchanged
when changing frequency or phase. The DDS phase is the sum of the
phase accumulator and the phase offset. The only discontinuous
changes in the DDS output phase come from changes to the phase
offset. This mode is also knows as "relative phase mode".
:math:`\phi(t) = q(t^\prime) + p + (t - t^\prime) f`
* :const:`PHASE_MODE_ABSOLUTE`: the phase accumulator is reset when
changing frequency or phase. Thus, the phase of the DDS at the
time of the change is equal to the specified phase offset.
:math:`\phi(t) = p + (t - t^\prime) f`
* :const:`PHASE_MODE_TRACKING`: when changing frequency or phase,
the phase accumulator is cleared and the phase offset is offset
by the value the phase accumulator would have if the DDS had been
running at the specified frequency since a given fiducial
time stamp. This is functionally equivalent to
:const:`PHASE_MODE_ABSOLUTE`. The only difference is the fiducial
time stamp. This mode is also known as "coherent phase mode".
:math:`\phi(t) = p + (t - T) f`
Where:
* :math:`\phi(t)`: the DDS output phase
* :math:`q(t) = \phi(t) - p`: DDS internal phase accumulator
* :math:`p`: phase offset
* :math:`f`: frequency
* :math:`t^\prime`: time stamp of setting :math:`p`, :math:`f`
* :math:`T`: fiducial time stamp
* :math:`t`: running time
.. warning:: This setting may become inconsistent when used as part of
a DMA recording. When using DMA, it is recommended to specify the
phase mode explicitly when calling :meth:`set` or :meth:`set_mu`.
"""
self.phase_mode = phase_mode
@kernel
def write32(self, addr, data):
@ -190,20 +248,53 @@ class AD9910:
self.cpld.io_update.pulse(1*us)
@kernel
def set_mu(self, ftw, pow=0, asf=0x3fff):
def set_mu(self, ftw, pow=0, asf=0x3fff, phase_mode=_PHASE_MODE_DEFAULT,
ref_time=-1):
"""Set profile 0 data in machine units.
This uses machine units (FTW, POW, ASF). The frequency tuning word
width is 32, the phase offset word width is 16, and the amplitude
scale factor width is 12.
After the SPI transfer, the shared IO update pin is pulsed to
activate the data.
.. seealso: :meth:`set_phase_mode` for a definition of the different
phase modes.
:param ftw: Frequency tuning word: 32 bit.
:param pow: Phase tuning word: 16 bit unsigned.
:param asf: Amplitude scale factor: 14 bit unsigned.
:param phase_mode: If specified, overrides the default phase mode set
by :meth:`set_phase_mode` for this call.
:param ref_time: Fiducial time used to compute absolute or tracking
phase updates. In machine units as obtained by `now_mu()`.
:return: Resulting phase offset word after application of phase
tracking offset. When using :const:`PHASE_MODE_CONTINUOUS` in
subsequent calls, use this value as the "current" phase.
"""
if phase_mode == _PHASE_MODE_DEFAULT:
phase_mode = self.phase_mode
# Align to coarse RTIO which aligns SYNC_CLK
at_mu(now_mu() & ~0xf)
if phase_mode != PHASE_MODE_CONTINUOUS:
# Auto-clear phase accumulator on IO_UPDATE.
# This is active already for the next IO_UPDATE
self.write32(_AD9910_REG_CFR1, 0x00002002)
if ref_time >= 0:
# 32 LSB are sufficient.
# Also no need to use IO_UPDATE time as this
# is equivalent to an output pipeline latency.
dt = int32(now_mu()) - int32(ref_time)
pow += dt*ftw*self.sysclk_per_mu >> 16
self.write64(_AD9910_REG_PR0, (asf << 16) | pow, ftw)
# align IO_UPDATE to SYNC_CLK
at_mu((now_mu() & ~0xf) | self.io_update_delay)
self.cpld.io_update.pulse_mu(8)
delay_mu(int64(self.io_update_delay))
self.cpld.io_update.pulse_mu(8) # assumes 8 mu > t_SYSCLK
at_mu(now_mu() & ~0xf)
if phase_mode != PHASE_MODE_CONTINUOUS:
self.write32(_AD9910_REG_CFR1, 0x00000002)
# future IO_UPDATE will activate
return pow
@portable(flags={"fast-math"})
def frequency_to_ftw(self, frequency):
@ -223,8 +314,15 @@ class AD9910:
"""Returns amplitude scale factor corresponding to given amplitude."""
return int32(round(amplitude*0x3ffe))
@portable(flags={"fast-math"})
def pow_to_turns(self, pow):
"""Returns the phase in turns corresponding to a given phase offset
word."""
return pow/0x10000
@kernel
def set(self, frequency, phase=0.0, amplitude=1.0):
def set(self, frequency, phase=0.0, amplitude=1.0,
phase_mode=_PHASE_MODE_DEFAULT, ref_time=-1):
"""Set profile 0 data in SI units.
.. seealso:: :meth:`set_mu`
@ -232,10 +330,13 @@ class AD9910:
:param ftw: Frequency in Hz
:param pow: Phase tuning word in turns
:param asf: Amplitude in units of full scale
:param phase_mode: Phase mode constant
:param ref_time: Fiducial time stamp in machine units
:return: Resulting phase offset in turns
"""
self.set_mu(self.frequency_to_ftw(frequency),
self.turns_to_pow(phase),
self.amplitude_to_asf(amplitude))
return self.pow_to_turns(self.set_mu(
self.frequency_to_ftw(frequency), self.turns_to_pow(phase),
self.amplitude_to_asf(amplitude), phase_mode, ref_time))
@kernel
def set_att_mu(self, att):
@ -352,7 +453,7 @@ class AD9910:
IO_UPDATE and SYNC_CLK.
The ramp generator is set up to a linear frequency ramp
(dFTW/t_SYNC_CLK=1) and started at a RTIO timestamp.
(dFTW/t_SYNC_CLK=1) and started at a RTIO time stamp.
After scanning the alignment, an IO_UPDATE delay midway between two
edges should be chosen.