mirror of https://github.com/m-labs/artiq.git
coredevice: add new ad9914 driver
This commit is contained in:
parent
f383a470fe
commit
c8d91b297d
|
@ -0,0 +1,307 @@
|
||||||
|
"""
|
||||||
|
Driver for the AD9914 DDS (with parallel bus) on RTIO.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from artiq.language.core import *
|
||||||
|
from artiq.language.types import *
|
||||||
|
from artiq.language.units import *
|
||||||
|
from artiq.coredevice.rtio import rtio_output
|
||||||
|
|
||||||
|
from numpy import int32, int64
|
||||||
|
|
||||||
|
|
||||||
|
_PHASE_MODE_DEFAULT = -1
|
||||||
|
PHASE_MODE_CONTINUOUS = 0
|
||||||
|
PHASE_MODE_ABSOLUTE = 1
|
||||||
|
PHASE_MODE_TRACKING = 2
|
||||||
|
|
||||||
|
AD9914_REG_CFR1L = 0x01
|
||||||
|
AD9914_REG_CFR1H = 0x03
|
||||||
|
AD9914_REG_CFR2L = 0x05
|
||||||
|
AD9914_REG_CFR2H = 0x07
|
||||||
|
AD9914_REG_CFR3L = 0x09
|
||||||
|
AD9914_REG_CFR3H = 0x0b
|
||||||
|
AD9914_REG_CFR4L = 0x0d
|
||||||
|
AD9914_REG_CFR4H = 0x0f
|
||||||
|
AD9914_REG_DRGFL = 0x11
|
||||||
|
AD9914_REG_DRGFH = 0x13
|
||||||
|
AD9914_REG_DRGBL = 0x15
|
||||||
|
AD9914_REG_DRGBH = 0x17
|
||||||
|
AD9914_REG_DRGAL = 0x19
|
||||||
|
AD9914_REG_DRGAH = 0x1b
|
||||||
|
AD9914_REG_FTWL = 0x2d
|
||||||
|
AD9914_REG_FTWH = 0x2f
|
||||||
|
AD9914_REG_POW = 0x31
|
||||||
|
AD9914_REG_ASF = 0x33
|
||||||
|
AD9914_REG_USR0 = 0x6d
|
||||||
|
AD9914_FUD = 0x80
|
||||||
|
AD9914_GPIO = 0x81
|
||||||
|
|
||||||
|
|
||||||
|
class AD9914:
|
||||||
|
"""Driver for one AD9914 DDS channel.
|
||||||
|
|
||||||
|
The time cursor is not modified by any function in this class.
|
||||||
|
|
||||||
|
Output event replacement is not supported and issuing commands at the same
|
||||||
|
time is an error.
|
||||||
|
|
||||||
|
:param sysclk: DDS system frequency. The DDS system clock must be a
|
||||||
|
phase-locked multiple of the RTIO clock.
|
||||||
|
:param bus_channel: RTIO channel number of the DDS bus.
|
||||||
|
:param channel: channel number (on the bus) of the DDS device to control.
|
||||||
|
"""
|
||||||
|
|
||||||
|
kernel_invariants = {"core", "sysclk", "bus_channel", "channel",
|
||||||
|
"rtio_period_mu", "sysclk_per_mu", "write_duration_mu",
|
||||||
|
"dac_cal_duration_mu", "init_duration_mu", "init_sync_duration_mu",
|
||||||
|
"set_duration_mu", "set_x_duration_mu"
|
||||||
|
"continuous_phase_comp"}
|
||||||
|
|
||||||
|
def __init__(self, dmgr, sysclk, bus_channel, channel, core_device="core"):
|
||||||
|
self.core = dmgr.get(core_device)
|
||||||
|
self.sysclk = sysclk
|
||||||
|
self.bus_channel = bus_channel
|
||||||
|
self.channel = channel
|
||||||
|
self.phase_mode = PHASE_MODE_CONTINUOUS
|
||||||
|
|
||||||
|
self.rtio_period_mu = int64(8)
|
||||||
|
self.sysclk_per_mu = int32(self.sysclk * self.core.ref_period)
|
||||||
|
|
||||||
|
self.write_duration_mu = 5 * self.rtio_period_mu
|
||||||
|
self.dac_cal_duration_mu = 147000 * self.rtio_period_mu
|
||||||
|
self.init_duration_mu = 10 * self.write_duration_mu + self.dac_cal_duration_mu
|
||||||
|
self.init_sync_duration_mu = 18 * self.write_duration_mu + 2 * self.dac_cal_duration_mu
|
||||||
|
self.set_duration_mu = 6 * self.write_duration_mu
|
||||||
|
self.set_x_duration_mu = 7 * self.write_duration_mu
|
||||||
|
|
||||||
|
self.continuous_phase_comp = 0
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def write(self, addr, data):
|
||||||
|
rtio_output(now_mu(), self.bus_channel, addr, data)
|
||||||
|
delay_mu(self.write_duration_mu)
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def init(self):
|
||||||
|
"""Resets and initializes the DDS channel.
|
||||||
|
|
||||||
|
This needs to be done for each DDS channel before it can be used, and
|
||||||
|
it is recommended to use the startup kernel for this purpose.
|
||||||
|
"""
|
||||||
|
delay_mu(-self.init_duration_mu)
|
||||||
|
self.write(AD9914_GPIO, (1 << self.channel) << 1);
|
||||||
|
|
||||||
|
self.write(AD9914_REG_CFR1H, 0x0000) # Enable cosine output
|
||||||
|
self.write(AD9914_REG_CFR2L, 0x8900) # Enable matched latency
|
||||||
|
self.write(AD9914_REG_CFR2H, 0x0080) # Enable profile mode
|
||||||
|
self.write(AD9914_REG_DRGBH, 0x8000) # Programmable modulus B == 2**31
|
||||||
|
self.write(AD9914_REG_DRGBL, 0x0000)
|
||||||
|
self.write(AD9914_REG_ASF, 0x0fff) # Set amplitude to maximum
|
||||||
|
self.write(AD9914_REG_CFR4H, 0x0105) # Enable DAC calibration
|
||||||
|
self.write(AD9914_FUD, 0)
|
||||||
|
delay_mu(self.dac_cal_duration_mu)
|
||||||
|
self.write(AD9914_REG_CFR4H, 0x0005) # Disable DAC calibration
|
||||||
|
self.write(AD9914_FUD, 0)
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def init_sync(self, sync_delay):
|
||||||
|
"""Resets and initializes the DDS channel as well as configures
|
||||||
|
the AD9914 DDS for synchronisation. The synchronisation procedure
|
||||||
|
follows the steps outlined in the AN-1254 application note.
|
||||||
|
|
||||||
|
This needs to be done for each DDS channel before it can be used, and
|
||||||
|
it is recommended to use the startup kernel for this.
|
||||||
|
|
||||||
|
This function cannot be used in a batch; the correct way of
|
||||||
|
initializing multiple DDS channels is to call this function
|
||||||
|
sequentially with a delay between the calls. 10ms provides a good
|
||||||
|
timing margin.
|
||||||
|
|
||||||
|
:param sync_delay: integer from 0 to 0x3f that sets the value of
|
||||||
|
SYNC_OUT (bits 3-5) and SYNC_IN (bits 0-2) delay ADJ bits.
|
||||||
|
"""
|
||||||
|
delay_mu(-self.init_sync_duration_mu)
|
||||||
|
self.write(AD9914_GPIO, (1 << self.channel) << 1)
|
||||||
|
|
||||||
|
self.write(AD9914_REG_CFR4H, 0x0105) # Enable DAC calibration
|
||||||
|
self.write(AD9914_FUD, 0)
|
||||||
|
delay_mu(self.dac_cal_duration_mu)
|
||||||
|
self.write(AD9914_REG_CFR4H, 0x0005) # Disable DAC calibration
|
||||||
|
self.write(AD9914_FUD, 0)
|
||||||
|
self.write(AD9914_REG_CFR2L, 0x8b00) # Enable matched latency and sync_out
|
||||||
|
self.write(AD9914_FUD, 0)
|
||||||
|
# Set cal with sync and set sync_out and sync_in delay
|
||||||
|
self.write(AD9914_REG_USR0, 0x0840 | (sync_delay & 0x3f))
|
||||||
|
self.write(AD9914_FUD, 0)
|
||||||
|
self.write(AD9914_REG_CFR4H, 0x0105) # Enable DAC calibration
|
||||||
|
self.write(AD9914_FUD, 0)
|
||||||
|
delay_mu(self.dac_cal_duration_mu)
|
||||||
|
self.write(AD9914_REG_CFR4H, 0x0005) # Disable DAC calibration
|
||||||
|
self.write(AD9914_FUD, 0)
|
||||||
|
self.write(AD9914_REG_CFR1H, 0x0000) # Enable cosine output
|
||||||
|
self.write(AD9914_REG_CFR2H, 0x0080) # Enable profile mode
|
||||||
|
self.write(AD9914_REG_DRGBH, 0x8000) # Programmable modulus B == 2**31
|
||||||
|
self.write(AD9914_REG_DRGBL, 0x0000)
|
||||||
|
self.write(AD9914_REG_ASF, 0x0fff) # Set amplitude to maximum
|
||||||
|
self.write(AD9914_FUD, 0)
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def set_phase_mode(self, phase_mode):
|
||||||
|
"""Sets the phase mode of the DDS channel. Supported phase modes are:
|
||||||
|
|
||||||
|
* ``PHASE_MODE_CONTINUOUS``: the phase accumulator is unchanged when
|
||||||
|
switching frequencies. The DDS phase is the sum of the phase
|
||||||
|
accumulator and the phase offset. The only discrete jumps in the
|
||||||
|
DDS output phase come from changes to the phase offset.
|
||||||
|
|
||||||
|
* ``PHASE_MODE_ABSOLUTE``: the phase accumulator is reset when
|
||||||
|
switching frequencies. Thus, the phase of the DDS at the time of
|
||||||
|
the frequency change is equal to the phase offset.
|
||||||
|
|
||||||
|
* ``PHASE_MODE_TRACKING``: when switching frequencies, the phase
|
||||||
|
accumulator is set to the value it would have if the DDS had been
|
||||||
|
running at the specified frequency since the start of the
|
||||||
|
experiment.
|
||||||
|
"""
|
||||||
|
self.phase_mode = phase_mode
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def set_mu(self, frequency, phase=0, phase_mode=_PHASE_MODE_DEFAULT,
|
||||||
|
amplitude=0x0fff, ref_time=-1):
|
||||||
|
"""Sets the DDS channel to the specified frequency and phase.
|
||||||
|
|
||||||
|
This uses machine units (FTW and POW). The frequency tuning word width
|
||||||
|
is 32, whereas the phase offset word width depends on the type of DDS
|
||||||
|
chip and can be retrieved via the ``pow_width`` attribute. The amplitude
|
||||||
|
width is 12.
|
||||||
|
|
||||||
|
The "frequency update" pulse is sent to the DDS with a fixed latency
|
||||||
|
with respect to the current position of the time cursor.
|
||||||
|
|
||||||
|
:param frequency: frequency to generate.
|
||||||
|
:param phase: adds an offset, in turns, to the phase.
|
||||||
|
:param phase_mode: if specified, overrides the default phase mode set
|
||||||
|
by ``set_phase_mode`` for this call.
|
||||||
|
:param ref_time: reference time used to compute phase. Specifying this
|
||||||
|
makes it easier to have a well-defined phase relationship between
|
||||||
|
DDSes on the same bus that are updated at a similar time.
|
||||||
|
"""
|
||||||
|
if phase_mode == _PHASE_MODE_DEFAULT:
|
||||||
|
phase_mode = self.phase_mode
|
||||||
|
if ref_time < 0:
|
||||||
|
ref_time = now_mu()
|
||||||
|
delay_mu(-self.set_duration_mu)
|
||||||
|
|
||||||
|
self.write(AD9914_GPIO, (1 << self.channel) << 1)
|
||||||
|
|
||||||
|
self.write(AD9914_REG_FTWL, ftw & 0xffff)
|
||||||
|
self.write(AD9914_REG_FTWH, (ftw >> 16) & 0xffff)
|
||||||
|
|
||||||
|
# We need the RTIO fine timestamp clock to be phase-locked
|
||||||
|
# to DDS SYSCLK, and divided by an integer self.sysclk_per_mu.
|
||||||
|
if phase_mode == PHASE_MODE_CONTINUOUS:
|
||||||
|
# Do not clear phase accumulator on FUD
|
||||||
|
# Disable autoclear phase accumulator and enables OSK.
|
||||||
|
self.write(AD9914_REG_CFR1L, 0x0108)
|
||||||
|
pow += self.continuous_phase_comp
|
||||||
|
else:
|
||||||
|
# Clear phase accumulator on FUD
|
||||||
|
# Enable autoclear phase accumulator and enables OSK.
|
||||||
|
self.write(AD9914_REG_CFR1L, 0x2108)
|
||||||
|
fud_time = now_mu() + 2 * self.write_duration_mu
|
||||||
|
pow -= int32((ref_time - fud_time) * self.sysclk_per_mu * ftw >> (32 - 16))
|
||||||
|
if phase_mode == PHASE_MODE_TRACKING:
|
||||||
|
pow += int32(ref_time * self.sysclk_per_mu * ftw >> (32 - 16))
|
||||||
|
self.continuous_phase_comp = pow
|
||||||
|
|
||||||
|
self.write(AD9914_REG_POW, pow)
|
||||||
|
self.write(AD9914_REG_ASF, amplitude)
|
||||||
|
self.write(AD9914_FUD, 0)
|
||||||
|
|
||||||
|
@portable(flags={"fast-math"})
|
||||||
|
def frequency_to_ftw(self, frequency):
|
||||||
|
"""Returns the frequency tuning word corresponding to the given
|
||||||
|
frequency.
|
||||||
|
"""
|
||||||
|
return round(float(int64(2)**32*frequency/self.sysclk))
|
||||||
|
|
||||||
|
@portable(flags={"fast-math"})
|
||||||
|
def ftw_to_frequency(self, ftw):
|
||||||
|
"""Returns the frequency corresponding to the given frequency tuning
|
||||||
|
word.
|
||||||
|
"""
|
||||||
|
return ftw*self.sysclk/int64(2)**32
|
||||||
|
|
||||||
|
@portable(flags={"fast-math"})
|
||||||
|
def turns_to_pow(self, turns):
|
||||||
|
"""Returns the phase offset word corresponding to the given phase
|
||||||
|
in turns."""
|
||||||
|
return round(float(turns*2**16))
|
||||||
|
|
||||||
|
@portable(flags={"fast-math"})
|
||||||
|
def pow_to_turns(self, pow):
|
||||||
|
"""Returns the phase in turns corresponding to the given phase offset
|
||||||
|
word."""
|
||||||
|
return pow/2**16
|
||||||
|
|
||||||
|
@portable(flags={"fast-math"})
|
||||||
|
def amplitude_to_asf(self, amplitude):
|
||||||
|
"""Returns amplitude scale factor corresponding to given amplitude."""
|
||||||
|
return round(float(amplitude*0x0fff))
|
||||||
|
|
||||||
|
@portable(flags={"fast-math"})
|
||||||
|
def asf_to_amplitude(self, asf):
|
||||||
|
"""Returns the amplitude corresponding to the given amplitude scale
|
||||||
|
factor."""
|
||||||
|
return asf/0x0fff
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def set(self, frequency, phase=0.0, phase_mode=_PHASE_MODE_DEFAULT,
|
||||||
|
amplitude=1.0):
|
||||||
|
"""Like ``set_mu``, but uses Hz and turns."""
|
||||||
|
self.set_mu(self.frequency_to_ftw(frequency),
|
||||||
|
self.turns_to_pow(phase), phase_mode,
|
||||||
|
self.amplitude_to_asf(amplitude))
|
||||||
|
|
||||||
|
# Extended resolution functions
|
||||||
|
@kernel
|
||||||
|
def set_mu_x(self, xftw, amplitude=0x0fff):
|
||||||
|
delay_mu(-self.set_x_duration_mu)
|
||||||
|
|
||||||
|
self.write(AD9914_GPIO, (1 << self.channel) << 1)
|
||||||
|
|
||||||
|
# Enable programmable modulus.
|
||||||
|
# Note another undocumented "feature" of the AD9914:
|
||||||
|
# Programmable modulus breaks if the digital ramp enable bit is
|
||||||
|
# not set at the same time.
|
||||||
|
self.write(AD9914_REG_CFR2H, 0x0089)
|
||||||
|
self.write(AD9914_REG_DRGAL, xftw & 0xffff)
|
||||||
|
self.write(AD9914_REG_DRGAH, (xftw >> 16) & 0x7fff)
|
||||||
|
self.write(AD9914_REG_DRGFL, (xftw >> 31) & 0xffff)
|
||||||
|
self.write(AD9914_REG_DRGFH, (xftw >> 47) & 0xffff)
|
||||||
|
self.write(AD9914_REG_ASF, amplitude)
|
||||||
|
|
||||||
|
self.write(AD9914_FUD, 0)
|
||||||
|
|
||||||
|
@portable(flags={"fast-math"})
|
||||||
|
def frequency_to_xftw(self, frequency):
|
||||||
|
"""Returns the frequency tuning word corresponding to the given
|
||||||
|
frequency (extended resolution mode).
|
||||||
|
"""
|
||||||
|
return round(float(int64(2)**63*frequency/self.sysclk))
|
||||||
|
|
||||||
|
@portable(flags={"fast-math"})
|
||||||
|
def xftw_to_frequency(self, xftw):
|
||||||
|
"""Returns the frequency corresponding to the given frequency tuning
|
||||||
|
word (extended resolution mode).
|
||||||
|
"""
|
||||||
|
return xftw*self.sysclk/int64(2)**63
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def set_x(self, frequency, amplitude=1.0):
|
||||||
|
"""Like ``set_mu_x``, but uses Hz and turns."""
|
||||||
|
self.set_mu_x(self.frequency_to_xftw(frequency),
|
||||||
|
self.amplitude_to_asf(amplitude))
|
||||||
|
|
Loading…
Reference in New Issue