mirror of
https://github.com/m-labs/artiq.git
synced 2024-12-12 05:06:36 +08:00
1122 lines
43 KiB
Python
1122 lines
43 KiB
Python
from numpy import int32, int64
|
|
|
|
from artiq.language.core import (
|
|
kernel, delay, portable, delay_mu, now_mu, at_mu)
|
|
from artiq.language.units import us, ms
|
|
from artiq.language.types import TBool, TInt32, TInt64, TFloat, TList, TTuple
|
|
|
|
from artiq.coredevice import spi2 as spi
|
|
from artiq.coredevice import urukul
|
|
from artiq.coredevice.urukul import DEFAULT_PROFILE
|
|
|
|
# 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",
|
|
"RAM_DEST_FTW", "RAM_DEST_POW", "RAM_DEST_ASF", "RAM_DEST_POWASF",
|
|
"RAM_MODE_DIRECTSWITCH", "RAM_MODE_RAMPUP", "RAM_MODE_BIDIR_RAMP",
|
|
"RAM_MODE_CONT_BIDIR_RAMP", "RAM_MODE_CONT_RAMPUP",
|
|
]
|
|
|
|
_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
|
|
_AD9910_REG_AUX_DAC = 0x03
|
|
_AD9910_REG_IO_UPDATE = 0x04
|
|
_AD9910_REG_FTW = 0x07
|
|
_AD9910_REG_POW = 0x08
|
|
_AD9910_REG_ASF = 0x09
|
|
_AD9910_REG_SYNC = 0x0a
|
|
_AD9910_REG_RAMP_LIMIT = 0x0b
|
|
_AD9910_REG_RAMP_STEP = 0x0c
|
|
_AD9910_REG_RAMP_RATE = 0x0d
|
|
_AD9910_REG_PROFILE0 = 0x0e
|
|
_AD9910_REG_PROFILE1 = 0x0f
|
|
_AD9910_REG_PROFILE2 = 0x10
|
|
_AD9910_REG_PROFILE3 = 0x11
|
|
_AD9910_REG_PROFILE4 = 0x12
|
|
_AD9910_REG_PROFILE5 = 0x13
|
|
_AD9910_REG_PROFILE6 = 0x14
|
|
_AD9910_REG_PROFILE7 = 0x15
|
|
_AD9910_REG_RAM = 0x16
|
|
|
|
# RAM destination
|
|
RAM_DEST_FTW = 0
|
|
RAM_DEST_POW = 1
|
|
RAM_DEST_ASF = 2
|
|
RAM_DEST_POWASF = 3
|
|
|
|
# RAM MODES
|
|
RAM_MODE_DIRECTSWITCH = 0
|
|
RAM_MODE_RAMPUP = 1
|
|
RAM_MODE_BIDIR_RAMP = 2
|
|
RAM_MODE_CONT_BIDIR_RAMP = 3
|
|
RAM_MODE_CONT_RAMPUP = 4
|
|
|
|
# Default profile for RAM mode
|
|
_DEFAULT_PROFILE_RAM = 0
|
|
|
|
|
|
class SyncDataUser:
|
|
def __init__(self, core, sync_delay_seed, io_update_delay):
|
|
self.core = core
|
|
self.sync_delay_seed = sync_delay_seed
|
|
self.io_update_delay = io_update_delay
|
|
|
|
@kernel
|
|
def init(self):
|
|
pass
|
|
|
|
|
|
class SyncDataEeprom:
|
|
def __init__(self, dmgr, core, eeprom_str):
|
|
self.core = core
|
|
|
|
eeprom_device, eeprom_offset = eeprom_str.split(":")
|
|
self.eeprom_device = dmgr.get(eeprom_device)
|
|
self.eeprom_offset = int(eeprom_offset)
|
|
|
|
self.sync_delay_seed = 0
|
|
self.io_update_delay = 0
|
|
|
|
@kernel
|
|
def init(self):
|
|
word = self.eeprom_device.read_i32(self.eeprom_offset) >> 16
|
|
sync_delay_seed = word >> 8
|
|
if sync_delay_seed >= 0:
|
|
io_update_delay = word & 0xff
|
|
else:
|
|
io_update_delay = 0
|
|
if io_update_delay == 0xff: # unprogrammed EEPROM
|
|
io_update_delay = 0
|
|
# With Numpy, type(int32(-1) >> 1) == int64
|
|
self.sync_delay_seed = int32(sync_delay_seed)
|
|
self.io_update_delay = int32(io_update_delay)
|
|
|
|
|
|
class AD9910:
|
|
"""
|
|
AD9910 DDS channel on Urukul.
|
|
|
|
This class supports a single DDS channel and exposes the DDS,
|
|
the digital step attenuator, and the RF switch.
|
|
|
|
:param chip_select: Chip select configuration. On Urukul this is an
|
|
encoded chip select and not "one-hot": 3 to address multiple chips
|
|
(as configured through CFG_MASK_NU), 4-7 for individual channels.
|
|
:param cpld_device: Name of the Urukul CPLD this device is on.
|
|
:param sw_device: Name of the RF switch device. The RF switch is a
|
|
TTLOut channel available as the :attr:`sw` attribute of this instance.
|
|
:param pll_n: DDS PLL multiplier. The DDS sample clock is
|
|
f_ref/clk_div*pll_n where f_ref is the reference frequency and
|
|
clk_div is the reference clock divider (both set in the parent
|
|
Urukul CPLD instance).
|
|
:param pll_en: PLL enable bit, set to 0 to bypass PLL (default: 1).
|
|
Note that when bypassing the PLL the red front panel LED may remain on.
|
|
:param pll_cp: DDS PLL charge pump setting.
|
|
:param pll_vco: DDS PLL VCO range selection.
|
|
:param sync_delay_seed: SYNC_IN delay tuning starting value.
|
|
To stabilize the SYNC_IN delay tuning, run :meth:`tune_sync_delay` once
|
|
and set this to the delay tap number returned (default: -1 to signal no
|
|
synchronization and no tuning during :meth:`init`).
|
|
Can be a string of the form "eeprom_device:byte_offset" to read the
|
|
value from a I2C EEPROM; in which case, `io_update_delay` must be set
|
|
to the same string value.
|
|
:param io_update_delay: IO_UPDATE pulse alignment delay.
|
|
To align IO_UPDATE to SYNC_CLK, run :meth:`tune_io_update_delay` and
|
|
set this to the delay tap number returned.
|
|
Can be a string of the form "eeprom_device:byte_offset" to read the
|
|
value from a I2C EEPROM; in which case, `sync_delay_seed` must be set
|
|
to the same string value.
|
|
"""
|
|
|
|
def __init__(self, dmgr, chip_select, cpld_device, sw_device=None,
|
|
pll_n=40, pll_cp=7, pll_vco=5, sync_delay_seed=-1,
|
|
io_update_delay=0, pll_en=1):
|
|
self.kernel_invariants = {"cpld", "core", "bus", "chip_select",
|
|
"pll_en", "pll_n", "pll_vco", "pll_cp",
|
|
"ftw_per_hz", "sysclk_per_mu", "sysclk",
|
|
"sync_data"}
|
|
self.cpld = dmgr.get(cpld_device)
|
|
self.core = self.cpld.core
|
|
self.bus = self.cpld.bus
|
|
assert 3 <= chip_select <= 7
|
|
self.chip_select = chip_select
|
|
if sw_device:
|
|
self.sw = dmgr.get(sw_device)
|
|
self.kernel_invariants.add("sw")
|
|
clk = self.cpld.refclk / [4, 1, 2, 4][self.cpld.clk_div]
|
|
self.pll_en = pll_en
|
|
self.pll_n = pll_n
|
|
self.pll_vco = pll_vco
|
|
self.pll_cp = pll_cp
|
|
if pll_en:
|
|
sysclk = clk * pll_n
|
|
assert clk <= 60e6
|
|
assert 12 <= pll_n <= 127
|
|
assert 0 <= pll_vco <= 5
|
|
vco_min, vco_max = [(370, 510), (420, 590), (500, 700),
|
|
(600, 880), (700, 950), (820, 1150)][pll_vco]
|
|
assert vco_min <= sysclk / 1e6 <= vco_max
|
|
assert 0 <= pll_cp <= 7
|
|
else:
|
|
sysclk = clk
|
|
assert sysclk <= 1e9
|
|
self.ftw_per_hz = (1 << 32) / sysclk
|
|
self.sysclk_per_mu = int(round(sysclk * self.core.ref_period))
|
|
self.sysclk = sysclk
|
|
|
|
if isinstance(sync_delay_seed, str) or isinstance(io_update_delay,
|
|
str):
|
|
if sync_delay_seed != io_update_delay:
|
|
raise ValueError("When using EEPROM, sync_delay_seed must be "
|
|
"equal to io_update_delay")
|
|
self.sync_data = SyncDataEeprom(dmgr, self.core, sync_delay_seed)
|
|
else:
|
|
self.sync_data = SyncDataUser(self.core, sync_delay_seed,
|
|
io_update_delay)
|
|
|
|
self.phase_mode = PHASE_MODE_CONTINUOUS
|
|
|
|
@kernel
|
|
def set_phase_mode(self, phase_mode: TInt32):
|
|
r"""Set 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".
|
|
The default fiducial time stamp is 0.
|
|
: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 write16(self, addr: TInt32, data: TInt32):
|
|
"""Write to 16 bit register.
|
|
|
|
:param addr: Register address
|
|
:param data: Data to be written
|
|
"""
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 24,
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
|
self.bus.write((addr << 24) | ((data & 0xffff) << 8))
|
|
|
|
@kernel
|
|
def write32(self, addr: TInt32, data: TInt32):
|
|
"""Write to 32 bit register.
|
|
|
|
:param addr: Register address
|
|
:param data: Data to be written
|
|
"""
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG, 8,
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
|
self.bus.write(addr << 24)
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 32,
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
|
self.bus.write(data)
|
|
|
|
@kernel
|
|
def read16(self, addr: TInt32) -> TInt32:
|
|
"""Read from 16 bit register.
|
|
|
|
:param addr: Register address
|
|
"""
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG, 8,
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
|
self.bus.write((addr | 0x80) << 24)
|
|
self.bus.set_config_mu(
|
|
urukul.SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT,
|
|
16, urukul.SPIT_DDS_RD, self.chip_select)
|
|
self.bus.write(0)
|
|
return self.bus.read()
|
|
|
|
@kernel
|
|
def read32(self, addr: TInt32) -> TInt32:
|
|
"""Read from 32 bit register.
|
|
|
|
:param addr: Register address
|
|
"""
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG, 8,
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
|
self.bus.write((addr | 0x80) << 24)
|
|
self.bus.set_config_mu(
|
|
urukul.SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT,
|
|
32, urukul.SPIT_DDS_RD, self.chip_select)
|
|
self.bus.write(0)
|
|
return self.bus.read()
|
|
|
|
@kernel
|
|
def read64(self, addr: TInt32) -> TInt64:
|
|
"""Read from 64 bit register.
|
|
|
|
:param addr: Register address
|
|
:return: 64 bit integer register value
|
|
"""
|
|
self.bus.set_config_mu(
|
|
urukul.SPI_CONFIG, 8,
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
|
self.bus.write((addr | 0x80) << 24)
|
|
self.bus.set_config_mu(
|
|
urukul.SPI_CONFIG | spi.SPI_INPUT, 32,
|
|
urukul.SPIT_DDS_RD, self.chip_select)
|
|
self.bus.write(0)
|
|
self.bus.set_config_mu(
|
|
urukul.SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, 32,
|
|
urukul.SPIT_DDS_RD, self.chip_select)
|
|
self.bus.write(0)
|
|
hi = self.bus.read()
|
|
lo = self.bus.read()
|
|
return (int64(hi) << 32) | lo
|
|
|
|
@kernel
|
|
def write64(self, addr: TInt32, data_high: TInt32, data_low: TInt32):
|
|
"""Write to 64 bit register.
|
|
|
|
:param addr: Register address
|
|
:param data_high: High (MSB) 32 bits of the data
|
|
:param data_low: Low (LSB) 32 data bits
|
|
"""
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG, 8,
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
|
self.bus.write(addr << 24)
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG, 32,
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
|
self.bus.write(data_high)
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 32,
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
|
self.bus.write(data_low)
|
|
|
|
@kernel
|
|
def write_ram(self, data: TList(TInt32)):
|
|
"""Write data to RAM.
|
|
|
|
The profile to write to and the step, start, and end address
|
|
need to be configured before and separately using
|
|
:meth:`set_profile_ram` and the parent CPLD `set_profile`.
|
|
|
|
:param data: Data to be written to RAM.
|
|
"""
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG, 8, urukul.SPIT_DDS_WR,
|
|
self.chip_select)
|
|
self.bus.write(_AD9910_REG_RAM << 24)
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG, 32,
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
|
for i in range(len(data) - 1):
|
|
self.bus.write(data[i])
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 32,
|
|
urukul.SPIT_DDS_WR, self.chip_select)
|
|
self.bus.write(data[len(data) - 1])
|
|
|
|
@kernel
|
|
def read_ram(self, data: TList(TInt32)):
|
|
"""Read data from RAM.
|
|
|
|
The profile to read from and the step, start, and end address
|
|
need to be configured before and separately using
|
|
:meth:`set_profile_ram` and the parent CPLD `set_profile`.
|
|
|
|
:param data: List to be filled with data read from RAM.
|
|
"""
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG, 8, urukul.SPIT_DDS_WR,
|
|
self.chip_select)
|
|
self.bus.write((_AD9910_REG_RAM | 0x80) << 24)
|
|
n = len(data) - 1
|
|
if n > 0:
|
|
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_INPUT, 32,
|
|
urukul.SPIT_DDS_RD, self.chip_select)
|
|
preload = min(n, 8)
|
|
for i in range(n):
|
|
self.bus.write(0)
|
|
if i >= preload:
|
|
data[i - preload] = self.bus.read()
|
|
self.bus.set_config_mu(
|
|
urukul.SPI_CONFIG | spi.SPI_INPUT | spi.SPI_END, 32,
|
|
urukul.SPIT_DDS_RD, self.chip_select)
|
|
self.bus.write(0)
|
|
for i in range(preload + 1):
|
|
data[(n - preload) + i] = self.bus.read()
|
|
|
|
@kernel
|
|
def set_cfr1(self,
|
|
power_down: TInt32 = 0b0000,
|
|
phase_autoclear: 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.
|
|
:param ram_destination: RAM destination
|
|
(:const:`RAM_DEST_FTW`, :const:`RAM_DEST_POW`,
|
|
:const:`RAM_DEST_ASF`, :const:`RAM_DEST_POWASF`).
|
|
:param ram_enable: RAM mode enable.
|
|
:param manual_osk_external: Enable OSK pin control in manual OSK mode.
|
|
:param osk_enable: Enable OSK mode.
|
|
:param select_auto_osk: Select manual or automatic OSK mode.
|
|
"""
|
|
self.write32(_AD9910_REG_CFR1,
|
|
(ram_enable << 31) |
|
|
(ram_destination << 29) |
|
|
(manual_osk_external << 23) |
|
|
(internal_profile << 17) |
|
|
(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.
|
|
|
|
Sets up SPI mode, confirms chip presence, powers down unused blocks,
|
|
configures the PLL, waits for PLL lock. Uses the
|
|
IO_UPDATE signal multiple times.
|
|
|
|
:param blind: Do not read back DDS identity and do not wait for lock.
|
|
"""
|
|
self.sync_data.init()
|
|
if self.sync_data.sync_delay_seed >= 0 and not self.cpld.sync_div:
|
|
raise ValueError("parent cpld does not drive SYNC")
|
|
if self.sync_data.sync_delay_seed >= 0:
|
|
if self.sysclk_per_mu != self.sysclk * self.core.ref_period:
|
|
raise ValueError("incorrect clock ratio for synchronization")
|
|
delay(50 * ms) # slack
|
|
|
|
# Set SPI mode
|
|
self.set_cfr1()
|
|
self.cpld.io_update.pulse(1 * us)
|
|
delay(1 * ms)
|
|
if not blind:
|
|
# Use the AUX DAC setting to identify and confirm presence
|
|
aux_dac = self.read32(_AD9910_REG_AUX_DAC)
|
|
if aux_dac & 0xff != 0x7f:
|
|
raise ValueError("Urukul AD9910 AUX_DAC mismatch")
|
|
delay(50 * us) # slack
|
|
# Configure PLL settings and bring up PLL
|
|
# enable amplitude scale from profiles
|
|
# read effective FTW
|
|
# sync timing validation disable (enabled later)
|
|
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) |
|
|
(self.pll_n << 1))
|
|
self.write32(_AD9910_REG_CFR3, cfr3 | 0x400) # PFD reset
|
|
self.cpld.io_update.pulse(1 * us)
|
|
if self.pll_en:
|
|
self.write32(_AD9910_REG_CFR3, cfr3)
|
|
self.cpld.io_update.pulse(1 * us)
|
|
if blind:
|
|
delay(100 * ms)
|
|
else:
|
|
# Wait for PLL lock, up to 100 ms
|
|
for i in range(100):
|
|
sta = self.cpld.sta_read()
|
|
lock = urukul_sta_pll_lock(sta)
|
|
delay(1 * ms)
|
|
if lock & (1 << self.chip_select - 4):
|
|
break
|
|
if i >= 100 - 1:
|
|
raise ValueError("PLL lock timeout")
|
|
delay(10 * us) # slack
|
|
if self.sync_data.sync_delay_seed >= 0 and not blind:
|
|
self.tune_sync_delay(self.sync_data.sync_delay_seed)
|
|
delay(1 * ms)
|
|
|
|
@kernel
|
|
def power_down(self, bits: TInt32 = 0b1111):
|
|
"""Power down DDS.
|
|
|
|
:param bits: Power down bits, see datasheet
|
|
"""
|
|
self.set_cfr1(power_down=bits)
|
|
self.cpld.io_update.pulse(1 * us)
|
|
|
|
@kernel
|
|
def set_mu(self, ftw: TInt32 = 0, pow_: TInt32 = 0, asf: TInt32 = 0x3fff,
|
|
phase_mode: TInt32 = _PHASE_MODE_DEFAULT,
|
|
ref_time_mu: TInt64 = int64(-1),
|
|
profile: TInt32 = DEFAULT_PROFILE,
|
|
ram_destination: TInt32 = -1) -> TInt32:
|
|
"""Set DDS 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 14.
|
|
|
|
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_mu: Fiducial time used to compute absolute or tracking
|
|
phase updates. In machine units as obtained by `now_mu()`.
|
|
:param profile: Single tone profile number to set (0-7, default: 7).
|
|
Ineffective if `ram_destination` is specified.
|
|
:param ram_destination: RAM destination (:const:`RAM_DEST_FTW`,
|
|
:const:`RAM_DEST_POW`, :const:`RAM_DEST_ASF`,
|
|
:const:`RAM_DEST_POWASF`). If specified, write free DDS parameters
|
|
to the ASF/FTW/POW registers instead of to the single tone profile
|
|
register (default behaviour, see `profile`).
|
|
: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. I.e. clear fine TSC
|
|
# This will not cause a collision or sequence error.
|
|
at_mu(now_mu() & ~7)
|
|
if phase_mode != PHASE_MODE_CONTINUOUS:
|
|
# Auto-clear phase accumulator on IO_UPDATE.
|
|
# This is active already for the next IO_UPDATE
|
|
self.set_cfr1(phase_autoclear=1)
|
|
if phase_mode == PHASE_MODE_TRACKING and ref_time_mu < 0:
|
|
# set default fiducial time stamp
|
|
ref_time_mu = 0
|
|
if ref_time_mu >= 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_mu)
|
|
pow_ += dt * ftw * self.sysclk_per_mu >> 16
|
|
if ram_destination == -1:
|
|
self.write64(_AD9910_REG_PROFILE0 + profile,
|
|
(asf << 16) | (pow_ & 0xffff), ftw)
|
|
else:
|
|
if not ram_destination == RAM_DEST_FTW:
|
|
self.set_ftw(ftw)
|
|
if not ram_destination == RAM_DEST_POWASF:
|
|
if not ram_destination == RAM_DEST_ASF:
|
|
self.set_asf(asf)
|
|
if not ram_destination == RAM_DEST_POW:
|
|
self.set_pow(pow_)
|
|
delay_mu(int64(self.sync_data.io_update_delay))
|
|
self.cpld.io_update.pulse_mu(8) # assumes 8 mu > t_SYN_CCLK
|
|
at_mu(now_mu() & ~7) # clear fine TSC again
|
|
if phase_mode != PHASE_MODE_CONTINUOUS:
|
|
self.set_cfr1()
|
|
# future IO_UPDATE will activate
|
|
return pow_
|
|
|
|
@kernel
|
|
def get_mu(self, profile: TInt32 = DEFAULT_PROFILE
|
|
) -> TTuple([TInt32, TInt32, TInt32]):
|
|
"""Get the frequency tuning word, phase offset word,
|
|
and amplitude scale factor.
|
|
|
|
.. seealso:: :meth:`get`
|
|
|
|
:param profile: Profile number to get (0-7, default: 7)
|
|
:return: A tuple ``(ftw, pow, asf)``
|
|
"""
|
|
|
|
# Read data
|
|
data = int64(self.read64(_AD9910_REG_PROFILE0 + profile))
|
|
# Extract and return fields
|
|
ftw = int32(data)
|
|
pow_ = int32((data >> 32) & 0xffff)
|
|
asf = int32((data >> 48) & 0x3fff)
|
|
return ftw, pow_, asf
|
|
|
|
@kernel
|
|
def set_profile_ram(self, start: TInt32, end: TInt32, step: TInt32 = 1,
|
|
profile: TInt32 = _DEFAULT_PROFILE_RAM,
|
|
nodwell_high: TInt32 = 0, zero_crossing: TInt32 = 0,
|
|
mode: TInt32 = 1):
|
|
"""Set the RAM profile settings.
|
|
|
|
:param start: Profile start address in RAM.
|
|
:param end: Profile end address in RAM (last address).
|
|
:param step: Profile time step in units of t_DDS, typically 4 ns
|
|
(default: 1).
|
|
:param profile: Profile index (0 to 7) (default: 0).
|
|
:param nodwell_high: No-dwell high bit (default: 0,
|
|
see AD9910 documentation).
|
|
:param zero_crossing: Zero crossing bit (default: 0,
|
|
see AD9910 documentation).
|
|
:param mode: Profile RAM mode (:const:`RAM_MODE_DIRECTSWITCH`,
|
|
:const:`RAM_MODE_RAMPUP`, :const:`RAM_MODE_BIDIR_RAMP`,
|
|
:const:`RAM_MODE_CONT_BIDIR_RAMP`, or
|
|
:const:`RAM_MODE_CONT_RAMPUP`, default:
|
|
:const:`RAM_MODE_RAMPUP`)
|
|
"""
|
|
hi = (step << 8) | (end >> 2)
|
|
lo = ((end << 30) | (start << 14) | (nodwell_high << 5) |
|
|
(zero_crossing << 3) | mode)
|
|
self.write64(_AD9910_REG_PROFILE0 + profile, hi, lo)
|
|
|
|
@kernel
|
|
def set_ftw(self, ftw: TInt32):
|
|
"""Set the value stored to the AD9910's frequency tuning word (FTW)
|
|
register.
|
|
|
|
:param ftw: Frequency tuning word to be stored, range: 0 to 0xffffffff.
|
|
"""
|
|
self.write32(_AD9910_REG_FTW, ftw)
|
|
|
|
@kernel
|
|
def set_asf(self, asf: TInt32):
|
|
"""Set the value stored to the AD9910's amplitude scale factor (ASF)
|
|
register.
|
|
|
|
:param asf: Amplitude scale factor to be stored, range: 0 to 0x3fff.
|
|
"""
|
|
self.write32(_AD9910_REG_ASF, asf << 2)
|
|
|
|
@kernel
|
|
def set_pow(self, pow_: TInt32):
|
|
"""Set the value stored to the AD9910's phase offset word (POW)
|
|
register.
|
|
|
|
:param pow_: Phase offset word to be stored, range: 0 to 0xffff.
|
|
"""
|
|
self.write16(_AD9910_REG_POW, pow_)
|
|
|
|
@kernel
|
|
def get_ftw(self) -> TInt32:
|
|
"""Get the value stored to the AD9910's frequency tuning word (FTW)
|
|
register.
|
|
|
|
:return: Frequency tuning word
|
|
"""
|
|
return self.read32(_AD9910_REG_FTW)
|
|
|
|
@kernel
|
|
def get_asf(self) -> TInt32:
|
|
"""Get the value stored to the AD9910's amplitude scale factor (ASF)
|
|
register.
|
|
|
|
:return: Amplitude scale factor
|
|
"""
|
|
return self.read32(_AD9910_REG_ASF) >> 2
|
|
|
|
@kernel
|
|
def get_pow(self) -> TInt32:
|
|
"""Get the value stored to the AD9910's phase offset word (POW)
|
|
register.
|
|
|
|
:return: Phase offset word
|
|
"""
|
|
return self.read16(_AD9910_REG_POW)
|
|
|
|
@portable(flags={"fast-math"})
|
|
def frequency_to_ftw(self, frequency: TFloat) -> TInt32:
|
|
"""Return the 32-bit frequency tuning word corresponding to the given
|
|
frequency.
|
|
"""
|
|
return int32(round(self.ftw_per_hz * frequency))
|
|
|
|
@portable(flags={"fast-math"})
|
|
def ftw_to_frequency(self, ftw: TInt32) -> TFloat:
|
|
"""Return the frequency corresponding to the given frequency tuning
|
|
word.
|
|
"""
|
|
return ftw / self.ftw_per_hz
|
|
|
|
@portable(flags={"fast-math"})
|
|
def turns_to_pow(self, turns: TFloat) -> TInt32:
|
|
"""Return the 16-bit phase offset word corresponding to the given phase
|
|
in turns."""
|
|
return int32(round(turns * 0x10000)) & int32(0xffff)
|
|
|
|
@portable(flags={"fast-math"})
|
|
def pow_to_turns(self, pow_: TInt32) -> TFloat:
|
|
"""Return the phase in turns corresponding to a given phase offset
|
|
word."""
|
|
return pow_ / 0x10000
|
|
|
|
@portable(flags={"fast-math"})
|
|
def amplitude_to_asf(self, amplitude: TFloat) -> TInt32:
|
|
"""Return 14-bit amplitude scale factor corresponding to given
|
|
fractional amplitude."""
|
|
code = int32(round(amplitude * 0x3fff))
|
|
if code < 0 or code > 0x3fff:
|
|
raise ValueError("Invalid AD9910 fractional amplitude!")
|
|
return code
|
|
|
|
@portable(flags={"fast-math"})
|
|
def asf_to_amplitude(self, asf: TInt32) -> TFloat:
|
|
"""Return amplitude as a fraction of full scale corresponding to given
|
|
amplitude scale factor."""
|
|
return asf / float(0x3fff)
|
|
|
|
@portable(flags={"fast-math"})
|
|
def frequency_to_ram(self, frequency: TList(TFloat), ram: TList(TInt32)):
|
|
"""Convert frequency values to RAM profile data.
|
|
|
|
To be used with :const:`RAM_DEST_FTW`.
|
|
|
|
:param frequency: List of frequency values in Hz.
|
|
:param ram: List to write RAM data into.
|
|
Suitable for :meth:`write_ram`.
|
|
"""
|
|
for i in range(len(ram)):
|
|
ram[i] = self.frequency_to_ftw(frequency[i])
|
|
|
|
@portable(flags={"fast-math"})
|
|
def turns_to_ram(self, turns: TList(TFloat), ram: TList(TInt32)):
|
|
"""Convert phase values to RAM profile data.
|
|
|
|
To be used with :const:`RAM_DEST_POW`.
|
|
|
|
:param turns: List of phase values in turns.
|
|
:param ram: List to write RAM data into.
|
|
Suitable for :meth:`write_ram`.
|
|
"""
|
|
for i in range(len(ram)):
|
|
ram[i] = self.turns_to_pow(turns[i]) << 16
|
|
|
|
@portable(flags={"fast-math"})
|
|
def amplitude_to_ram(self, amplitude: TList(TFloat), ram: TList(TInt32)):
|
|
"""Convert amplitude values to RAM profile data.
|
|
|
|
To be used with :const:`RAM_DEST_ASF`.
|
|
|
|
:param amplitude: List of amplitude values in units of full scale.
|
|
:param ram: List to write RAM data into.
|
|
Suitable for :meth:`write_ram`.
|
|
"""
|
|
for i in range(len(ram)):
|
|
ram[i] = self.amplitude_to_asf(amplitude[i]) << 18
|
|
|
|
@portable(flags={"fast-math"})
|
|
def turns_amplitude_to_ram(self, turns: TList(TFloat),
|
|
amplitude: TList(TFloat), ram: TList(TInt32)):
|
|
"""Convert phase and amplitude values to RAM profile data.
|
|
|
|
To be used with :const:`RAM_DEST_POWASF`.
|
|
|
|
:param turns: List of phase values in turns.
|
|
:param amplitude: List of amplitude values in units of full scale.
|
|
:param ram: List to write RAM data into.
|
|
Suitable for :meth:`write_ram`.
|
|
"""
|
|
for i in range(len(ram)):
|
|
ram[i] = ((self.turns_to_pow(turns[i]) << 16) |
|
|
self.amplitude_to_asf(amplitude[i]) << 2)
|
|
|
|
@kernel
|
|
def set_frequency(self, frequency: TFloat):
|
|
"""Set the value stored to the AD9910's frequency tuning word (FTW)
|
|
register.
|
|
|
|
:param frequency: frequency to be stored, in Hz.
|
|
"""
|
|
self.set_ftw(self.frequency_to_ftw(frequency))
|
|
|
|
@kernel
|
|
def set_amplitude(self, amplitude: TFloat):
|
|
"""Set the value stored to the AD9910's amplitude scale factor (ASF)
|
|
register.
|
|
|
|
:param amplitude: amplitude to be stored, in units of full scale.
|
|
"""
|
|
self.set_asf(self.amplitude_to_asf(amplitude))
|
|
|
|
@kernel
|
|
def set_phase(self, turns: TFloat):
|
|
"""Set the value stored to the AD9910's phase offset word (POW)
|
|
register.
|
|
|
|
:param turns: phase offset to be stored, in turns.
|
|
"""
|
|
self.set_pow(self.turns_to_pow(turns))
|
|
|
|
@kernel
|
|
def get_frequency(self) -> TFloat:
|
|
"""Get the value stored to the AD9910's frequency tuning word (FTW)
|
|
register.
|
|
|
|
:return: frequency in Hz.
|
|
"""
|
|
return self.ftw_to_frequency(self.get_ftw())
|
|
|
|
@kernel
|
|
def get_amplitude(self) -> TFloat:
|
|
"""Get the value stored to the AD9910's amplitude scale factor (ASF)
|
|
register.
|
|
|
|
:return: amplitude in units of full scale.
|
|
"""
|
|
return self.asf_to_amplitude(self.get_asf())
|
|
|
|
@kernel
|
|
def get_phase(self) -> TFloat:
|
|
"""Get the value stored to the AD9910's phase offset word (POW)
|
|
register.
|
|
|
|
:return: phase offset in turns.
|
|
"""
|
|
return self.pow_to_turns(self.get_pow())
|
|
|
|
@kernel
|
|
def set(self, frequency: TFloat = 0.0, phase: TFloat = 0.0,
|
|
amplitude: TFloat = 1.0, phase_mode: TInt32 = _PHASE_MODE_DEFAULT,
|
|
ref_time_mu: TInt64 = int64(-1), profile: TInt32 = DEFAULT_PROFILE,
|
|
ram_destination: TInt32 = -1) -> TFloat:
|
|
"""Set DDS data in SI units.
|
|
|
|
.. seealso:: :meth:`set_mu`
|
|
|
|
:param frequency: Frequency in Hz
|
|
:param phase: Phase tuning word in turns
|
|
:param amplitude: Amplitude in units of full scale
|
|
:param phase_mode: Phase mode constant
|
|
:param ref_time_mu: Fiducial time stamp in machine units
|
|
:param profile: Single tone profile to affect.
|
|
:param ram_destination: RAM destination.
|
|
:return: Resulting phase offset in turns
|
|
"""
|
|
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_mu,
|
|
profile, ram_destination))
|
|
|
|
@kernel
|
|
def get(self, profile: TInt32 = DEFAULT_PROFILE
|
|
) -> TTuple([TFloat, TFloat, TFloat]):
|
|
"""Get the frequency, phase, and amplitude.
|
|
|
|
.. seealso:: :meth:`get_mu`
|
|
|
|
:param profile: Profile number to get (0-7, default: 7)
|
|
:return: A tuple ``(frequency, phase, amplitude)``
|
|
"""
|
|
|
|
# Get values
|
|
ftw, pow_, asf = self.get_mu(profile)
|
|
# Convert and return
|
|
return (self.ftw_to_frequency(ftw), self.pow_to_turns(pow_),
|
|
self.asf_to_amplitude(asf))
|
|
|
|
@kernel
|
|
def set_att_mu(self, att: TInt32):
|
|
"""Set digital step attenuator in machine units.
|
|
|
|
This method will write the attenuator settings of all four channels.
|
|
|
|
.. seealso:: :meth:`artiq.coredevice.urukul.CPLD.set_att_mu`
|
|
|
|
:param att: Attenuation setting, 8 bit digital.
|
|
"""
|
|
self.cpld.set_att_mu(self.chip_select - 4, att)
|
|
|
|
@kernel
|
|
def set_att(self, att: TFloat):
|
|
"""Set digital step attenuator in SI units.
|
|
|
|
This method will write the attenuator settings of all four channels.
|
|
|
|
.. seealso:: :meth:`artiq.coredevice.urukul.CPLD.set_att`
|
|
|
|
:param att: Attenuation in dB.
|
|
"""
|
|
self.cpld.set_att(self.chip_select - 4, att)
|
|
|
|
@kernel
|
|
def get_att_mu(self) -> TInt32:
|
|
"""Get digital step attenuator value in machine units.
|
|
|
|
.. seealso:: :meth:`artiq.coredevice.urukul.CPLD.get_channel_att_mu`
|
|
|
|
:return: Attenuation setting, 8 bit digital.
|
|
"""
|
|
return self.cpld.get_channel_att_mu(self.chip_select - 4)
|
|
|
|
@kernel
|
|
def get_att(self) -> TFloat:
|
|
"""Get digital step attenuator value in SI units.
|
|
|
|
.. seealso:: :meth:`artiq.coredevice.urukul.CPLD.get_channel_att`
|
|
|
|
:return: Attenuation in dB.
|
|
"""
|
|
return self.cpld.get_channel_att(self.chip_select - 4)
|
|
|
|
@kernel
|
|
def cfg_sw(self, state: TBool):
|
|
"""Set CPLD CFG RF switch state. The RF switch is controlled by the
|
|
logical or of the CPLD configuration shift register
|
|
RF switch bit and the SW TTL line (if used).
|
|
|
|
:param state: CPLD CFG RF switch bit
|
|
"""
|
|
self.cpld.cfg_sw(self.chip_select - 4, state)
|
|
|
|
@kernel
|
|
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 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
|
|
(en_sync_gen << 26) | # SYNC generator enable
|
|
(0 << 25) | # SYNC generator SYS rising edge
|
|
(0 << 18) | # SYNC preset
|
|
(0 << 11) | # SYNC output delay
|
|
(in_delay << 3)) # SYNC receiver delay
|
|
|
|
@kernel
|
|
def clear_smp_err(self):
|
|
"""Clear the SMP_ERR flag and enables SMP_ERR validity monitoring.
|
|
|
|
Violations of the SYNC_IN sample and hold margins will result in
|
|
SMP_ERR being asserted. This then also activates the red LED on
|
|
the respective Urukul channel.
|
|
|
|
Also modifies CFR2.
|
|
"""
|
|
self.set_cfr2(sync_validation_disable=1) # clear SMP_ERR
|
|
self.cpld.io_update.pulse(1 * us)
|
|
delay(10 * us) # slack
|
|
self.set_cfr2(sync_validation_disable=0) # enable SMP_ERR
|
|
self.cpld.io_update.pulse(1 * us)
|
|
|
|
@kernel
|
|
def tune_sync_delay(self,
|
|
search_seed: TInt32 = 15) -> TTuple([TInt32, TInt32]):
|
|
"""Find a stable SYNC_IN delay.
|
|
|
|
This method first locates a valid SYNC_IN delay at zero validation
|
|
window size (setup/hold margin) by scanning around `search_seed`. It
|
|
then looks for similar valid delays at successively larger validation
|
|
window sizes until none can be found. It then decreases the validation
|
|
window a bit to provide some slack and stability and returns the
|
|
optimal values.
|
|
|
|
This method and :meth:`tune_io_update_delay` can be run in any order.
|
|
|
|
:param search_seed: Start value for valid SYNC_IN delay search.
|
|
Defaults to 15 (half range).
|
|
:return: Tuple of optimal delay and window size.
|
|
"""
|
|
if not self.cpld.sync_div:
|
|
raise ValueError("parent cpld does not drive SYNC")
|
|
search_span = 31
|
|
# FIXME https://github.com/sinara-hw/Urukul/issues/16
|
|
# should both be 2-4 once kasli sync_in jitter is identified
|
|
min_window = 0
|
|
margin = 1 # 1*75ps setup and hold
|
|
for window in range(16):
|
|
next_seed = -1
|
|
for in_delay in range(search_span - 2 * window):
|
|
# alternate search direction around search_seed
|
|
if in_delay & 1:
|
|
in_delay = -in_delay
|
|
in_delay = search_seed + (in_delay >> 1)
|
|
if in_delay < 0 or in_delay > 31:
|
|
continue
|
|
self.set_sync(in_delay, window)
|
|
self.clear_smp_err()
|
|
# integrate SMP_ERR statistics for a few hundred cycles
|
|
delay(100 * us)
|
|
err = urukul_sta_smp_err(self.cpld.sta_read())
|
|
delay(100 * us) # slack
|
|
if not (err >> (self.chip_select - 4)) & 1:
|
|
next_seed = in_delay
|
|
break
|
|
if next_seed >= 0: # valid delay found, scan next window
|
|
search_seed = next_seed
|
|
continue
|
|
elif window > min_window:
|
|
# no valid delay found here, roll back and add margin
|
|
window = max(min_window, window - 1 - margin)
|
|
self.set_sync(search_seed, window)
|
|
self.clear_smp_err()
|
|
delay(100 * us) # slack
|
|
return search_seed, window
|
|
else:
|
|
break
|
|
raise ValueError("no valid window/delay")
|
|
|
|
@kernel
|
|
def measure_io_update_alignment(self, delay_start: TInt64,
|
|
delay_stop: TInt64) -> TInt32:
|
|
"""Use the digital ramp generator to locate the alignment between
|
|
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 coarse RTIO time stamp plus
|
|
`delay_start` and stopped at a coarse RTIO time stamp plus
|
|
`delay_stop`.
|
|
|
|
:param delay_start: Start IO_UPDATE delay in machine units.
|
|
:param delay_stop: Stop IO_UPDATE delay in machine units.
|
|
:return: Odd/even SYNC_CLK cycle indicator.
|
|
"""
|
|
# 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_REG_RAMP_LIMIT, -1, 0)
|
|
# DRCTL=0, dt=1 t_SYNC_CLK
|
|
self.write32(_AD9910_REG_RAMP_RATE, 0x00010000)
|
|
# dFTW = 1, (work around negative slope)
|
|
self.write64(_AD9910_REG_RAMP_STEP, -1, 0)
|
|
# 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.pulse_mu(16 - delay_start) # realign
|
|
# disable DRG autoclear and LRR on io_update
|
|
self.set_cfr1()
|
|
# stop DRG
|
|
self.write64(_AD9910_REG_RAMP_STEP, 0, 0)
|
|
at_mu(t + 0x1000 + delay_stop)
|
|
self.cpld.io_update.pulse_mu(16 - delay_stop) # realign
|
|
ftw = self.read32(_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(8)
|
|
return ftw & 1
|
|
|
|
@kernel
|
|
def tune_io_update_delay(self) -> TInt32:
|
|
"""Find a stable IO_UPDATE delay alignment.
|
|
|
|
Scan through increasing IO_UPDATE delays until a delay is found that
|
|
lets IO_UPDATE be registered in the next SYNC_CLK cycle. Return a
|
|
IO_UPDATE delay that is as far away from that SYNC_CLK edge
|
|
as possible.
|
|
|
|
This method assumes that the IO_UPDATE TTLOut device has one machine
|
|
unit resolution (SERDES).
|
|
|
|
This method and :meth:`tune_sync_delay` can be run in any order.
|
|
|
|
:return: Stable IO_UPDATE delay to be passed to the constructor
|
|
:class:`AD9910` via the device database.
|
|
"""
|
|
period = self.sysclk_per_mu * 4 # SYNC_CLK period
|
|
repeat = 100
|
|
for i in range(period):
|
|
t = 0
|
|
# check whether the sync edge is strictly between i, i+2
|
|
for j in range(repeat):
|
|
t += self.measure_io_update_alignment(i, i + 2)
|
|
if t != 0: # no certain edge
|
|
continue
|
|
# check left/right half: i,i+1 and i+1,i+2
|
|
t1 = [0, 0]
|
|
for j in range(repeat):
|
|
t1[0] += self.measure_io_update_alignment(i, i + 1)
|
|
t1[1] += self.measure_io_update_alignment(i + 1, i + 2)
|
|
if ((t1[0] == 0 and t1[1] == 0) or
|
|
(t1[0] == repeat and t1[1] == repeat)):
|
|
# edge is not close to i + 1, can't interpret result
|
|
raise ValueError(
|
|
"no clear IO_UPDATE-SYNC_CLK alignment edge found")
|
|
else:
|
|
# the good delay is period//2 after the edge
|
|
return (i + 1 + period // 2) & (period - 1)
|
|
raise ValueError("no IO_UPDATE-SYNC_CLK alignment edge found")
|