now_mu() affects Urukul AD9910's output #1702

Closed
opened 2026-01-18 19:06:15 +08:00 by Dimitris · 3 comments

Migrated from GitHub: #2776


Bug Report

        delay_mu(1)
        t_io_update_mu = now_mu() + T_write64_mu + io_update_delay_mu

makes Urukul AD9910 output this:

delay_mu_1_first.png


        t_io_update_mu = now_mu() + T_write64_mu + io_update_delay_mu
        delay_mu(1)

makes Urukul AD9910 output this:

now_mu_first.png

By the way: self.urukul0_ch0.tune_io_update_delay() and print(self.urukul0_ch0.sync_data.io_update_delay) both return 0.

Expected Behavior

Calling now_mu() does not affect AD9910 output under any circumstances.

Actual (undesired) Behavior

AD9910 output is clearly affected by call to now_mu().

Background info

  • I want to switch an Urukul AD9910 from a frequency (say 200 MHz) to its mirror frequency (800 MHz) without a phase discontinuity.
  • I have been receiving help in this endeavor in AD9910: Mirror frequencies invert sign of phase accumulation.
  • I thought I had figured everything out, but then I ran into the above bug.
  • My goal is to keep track of an AD9910's phase with nanosecond precision, so yes delay_mu(1) makes a huge difference.

My System

Experiment code that reproduces the bug

Trigger your oscilloscope on falling edge of self.ttl0 and set normal trigger mode and time span 100 ns.

from artiq.language.environment import EnvExperiment
from artiq.language.core import kernel, rpc, delay, delay_mu, now_mu, at_mu, parallel
from artiq.language.units import ns, us, ms, s, Hz, MHz, V
from artiq.language.types import TInt32, TInt64, TFloat, TStr, TBool, TList, TTuple
from artiq.coredevice.i2c import i2c_write_byte
from artiq.coredevice.kasli_i2c import port_mapping
from artiq.coredevice.ad9910 import _PHASE_MODE_DEFAULT, PHASE_MODE_CONTINUOUS, PHASE_MODE_ABSOLUTE, PHASE_MODE_TRACKING, _AD9910_REG_PROFILE0
from artiq.coredevice.urukul import DEFAULT_PROFILE
from numpy import int32, uint32, int64, uint64

# Maps Kasli EEM port indices that are visible on the PCB
# to actual electrical port(?) indices that need to be passed to the FPGA.
KASLI_I2C_BOARD_TO_PORT_MAPPING = [port%8 for port in port_mapping.values()]
# for `artiq.coredevice.i2c.i2c_write_byte(busno, busaddr, data, ack=True)`
# and `artiq.coredevice.i2c.i2c_read_byte(busno, busaddr)`
DIO_SMA_BUS_NUMBER = 0
DIO_SMA_BUS_ADDRESS = 0x7c # = 124 (decimal) or 01111100 (binary)

@rpc
def print_binary(number, type_cast=uint32, nr_bits=32, print_bits=False):
    print("binary  :", f"{type_cast(number):{int(nr_bits)}b}")
    if print_bits:
        bits = ""
        for i in range(nr_bits):
            bits = str(i % 10) + bits
        print("bits    :", bits)

class DRGAmplitudeTest(EnvExperiment):

    def build(self):
        self.setattr_device("core") # artiq.coredevice.core.Core
        self.setattr_device("core_cache") # artiq.coredevice.cache.CoreCache
        device_db = self.get_device_db() # dict, DO NOT EDIT!
        self.n_kasli_socs = 1 + len(device_db["core"]["arguments"]["satellite_cpu_targets"])
        self.setattr_device("i2c_switch0") # artiq.coredevice.i2c.I2CSwitch
        self.setattr_device("ttl0") # artiq.coredevice.ttl.TTLInOut
        self.setattr_device("ttl1") # artiq.coredevice.ttl.TTLInOut
        self.setattr_device("ttl2") # artiq.coredevice.ttl.TTLInOut
        self.setattr_device("ttl3") # artiq.coredevice.ttl.TTLInOut
        self.setattr_device("urukul0_cpld") # artiq.coredevice.urukul.CPLD
        self.setattr_device("urukul0_ch0") # artiq.coredevice.ad9910.AD9910
        self.urukul0_ch0.ftw = int32(0)
        self.urukul0_ch0.pow = int32(0)
        self.urukul0_ch0.asf = int32(0)
        self.urukul0_ch0.t_acc_start_mu = int64(0)
        self.urukul0_ch0.acc_pow = int64(0)

    @kernel
    def init(self):
        r"""
        Should be called once after every reboot or power-cycle of the Kasli (SoC).
        """
        for i in range(self.n_kasli_socs):
            while not self.core.get_rtio_destination_status(i):
                pass
        self.core.reset()
        self.core.break_realtime()
        self.i2c_switch0.set(channel = KASLI_I2C_BOARD_TO_PORT_MAPPING[0])
        i2c_write_byte(
            busno   = DIO_SMA_BUS_NUMBER,
            busaddr = DIO_SMA_BUS_ADDRESS,
            data    = 0
        )
        self.i2c_switch0.unset()
        self.core.break_realtime()
        for ttl in [self.ttl0, self.ttl1, self.ttl2, self.ttl3]:
            ttl.output()
            delay(1*us)
            ttl.off()
            delay(1*us)
        self.urukul0_cpld.init()
        self.urukul0_cpld.cfg_att_en_all(1)
        self.urukul0_ch0.sw.off()
        self.urukul0_ch0.init()
        self.urukul0_ch0.set_phase_mode(PHASE_MODE_CONTINUOUS)
        self.urukul0_ch0.set_att(0.0)
        self.core.wait_until_mu(now_mu())

    @kernel
    def frequency_to_uint32(self, frequency: TFloat) -> TInt32:
        """
        Linearly map frequency ∈ [0*GHz, 1*GHz] to an unsigned 32-bit integer {0,1,..., 2**32-1}.
        Hacking is necessary because the ARTIQ compiler does *not* know unsigned integers.

        :param frequency: Must be in the interval [0*GHz, 1*GHz].
        """
        if frequency < 0*Hz:
            raise ValueError("Invalid AD9910 frequency!")
        elif frequency < self.urukul0_ch0.sysclk / 2:
            return self.urukul0_ch0.frequency_to_ftw(frequency)
        elif frequency <= self.urukul0_ch0.sysclk:
            return -1 - self.urukul0_ch0.frequency_to_ftw(self.urukul0_ch0.sysclk - frequency)
        else:
            raise ValueError("Invalid AD9910 frequency!")
        return int32(0) # prevents compiler crash

    @kernel
    def set_mu(self, ftw: TInt32, pow_: TInt32, asf: TInt32,
               phase_mode: TInt32 = _PHASE_MODE_DEFAULT,
               ref_time_mu: TInt64 = int64(-1),
               profile: TInt32 = DEFAULT_PROFILE) -> TInt32:
        if phase_mode == _PHASE_MODE_DEFAULT:
            phase_mode = self.urukul0_ch0.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.urukul0_ch0.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.urukul0_ch0.sysclk_per_mu >> 16
        self.urukul0_ch0.write64(_AD9910_REG_PROFILE0 + profile,
                                 (asf << 16) | (pow_ & 0xffff), ftw)
        delay_mu(int64(self.urukul0_ch0.sync_data.io_update_delay))
        t_io_update_mu = now_mu()
        self.urukul0_ch0.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:
            # phase accumulator has been reset
            self.urukul0_ch0.acc_pow = 0
            self.urukul0_ch0.set_cfr1()
            # future IO_UPDATE will activate
        else:
            # calculate phase-offset word in AD9910's phase accumulator at rising flank of io_update
            dt_mu = t_io_update_mu - self.urukul0_ch0.t_acc_start_mu
            if self.urukul0_ch0.ftw >= 0:
                # regular frequency, so AD9910 has been incrementing phase accumulator
                self.urukul0_ch0.acc_pow += self.urukul0_ch0.ftw * dt_mu * self.urukul0_ch0.sysclk_per_mu
            else:
                # mirror frequency, so AD9910 has been decrementing phase accumulator
                # f_mirror + f = 1*GHz means that we just flip all of ftw's bits
                self.urukul0_ch0.acc_pow -= (~self.urukul0_ch0.ftw) * dt_mu * self.urukul0_ch0.sysclk_per_mu
        self.urukul0_ch0.t_acc_start_mu = t_io_update_mu
        self.urukul0_ch0.ftw = ftw
        self.urukul0_ch0.pow = pow_
        self.urukul0_ch0.asf = asf
        return pow_

    @kernel
    def mirror(self, debug_ttl):
        # align to coarse RTIO which aligns SYNC_CLK
        at_mu(now_mu() & ~7)
        # delay by 1 nanosecond, otherwise phase discontinuity
        # see also https://github.com/m-labs/artiq/issues/2776
        # pre-calculate RTIO timeline cursor at next rising flank of io_update
        T_write64_mu = 1248 # RTIO timeline cursor advancement per register-write
        io_update_delay_mu = int64(self.urukul0_ch0.sync_data.io_update_delay)
        t_io_update_mu = now_mu() + T_write64_mu + io_update_delay_mu
        delay_mu(1)
        # f_mirror + f = 1*GHz means that we just flip all of ftw's bits
        ftw_mirror = ~self.urukul0_ch0.ftw
        # pre-calculate phase-offset word of AD9910's phase accumulator at next rising flank of io_update
        dt_mu = t_io_update_mu - self.urukul0_ch0.t_acc_start_mu
        if self.urukul0_ch0.ftw >= 0:
            # regular frequency, so AD9910 has been incrementing phase accumulator
            self.urukul0_ch0.acc_pow += self.urukul0_ch0.ftw * dt_mu * self.urukul0_ch0.sysclk_per_mu
        else:
            # mirror frequency, so AD9910 has been decrementing phase accumulator
            self.urukul0_ch0.acc_pow -= ftw_mirror * dt_mu * self.urukul0_ch0.sysclk_per_mu
        # pre-calculate phase-offset word of AD9910's total output phase at next rising flank of io_update
        pow_io_update_mu = (self.urukul0_ch0.acc_pow >> 16) + self.urukul0_ch0.pow
        # mirror phase-offset word around a multiple of 2π
        self.urukul0_ch0.pow -= 2*pow_io_update_mu
        # write mirror frequency to single-tone register
        self.urukul0_ch0.ftw = ftw_mirror
        self.urukul0_ch0.write64(_AD9910_REG_PROFILE0 + 7,
                                 (self.urukul0_ch0.asf << 16) | (self.urukul0_ch0.pow & 0xffff), self.urukul0_ch0.ftw)
        delay_mu(io_update_delay_mu)
        # record new switching time
        self.urukul0_ch0.t_acc_start_mu = now_mu()
        # transfer mirror frequency to active output register
        self.urukul0_ch0.io_update.pulse_mu(8)
        delay_mu(84)
        debug_ttl.off()
        delay_mu(-84)
        # verify that we pre-calculated the accumulated phase correctly
        # if self.urukul0_ch0.t_acc_start_mu != t_io_update_mu:
        #     raise ValueError("You pre-calculated the RTIO time cursor wrongly, \
        #                      so you caused a phase discontinuity. Check on scope!")

    @kernel
    def dds_set(self, frequ: TFloat, turns: TFloat, amp: TFloat,
                phase_mode: TInt32 = _PHASE_MODE_DEFAULT,
                ref_time_mu: TInt64 = int64(-1)):
        self.set_mu(self.frequency_to_uint32(frequ),
                    self.urukul0_ch0.turns_to_pow(turns),
                    self.urukul0_ch0.amplitude_to_asf(amp),
                    phase_mode, ref_time_mu)

    @rpc(flags={"async"})
    def print_async(self, d):
        print("d =", d)

    @kernel
    def run(self):
        self.init()
        self.core.reset()
        self.core.break_realtime()
        for d in range(-20, 20+1, 1):
            for p in range(1):
                delay(50*ms)
                self.dds_set(100*MHz, 0.0, 0.1, PHASE_MODE_ABSOLUTE)
                delay(20*us)
                self.urukul0_ch0.sw.on()
                delay(151*ns)
                self.ttl0.on()
                self.ttl1.on()
                self.ttl2.on()
                delay(-1*us + d*ns)
                self.mirror(self.ttl0)
                delay_mu(434+8-5*d)
                self.mirror(self.ttl3)
                delay_mu(4321+2*d)
                self.mirror(self.ttl3)
                delay_mu(459+4*d)
                self.mirror(self.ttl3)
                delay_mu(3456+11*d)
                self.mirror(self.ttl3)
                # delay(2*s)
                delay_mu(2349+1*d)
                self.mirror(self.ttl3)
                delay_mu(2541-9*d)
                self.mirror(self.ttl3)
                delay_mu(984-4*d)
                self.mirror(self.ttl1)
                delay_mu(12345+3*d)
                self.mirror(self.ttl3)
                delay_mu(491+7*d)
                self.mirror(self.ttl3)
                delay_mu(42459-8*d)
                self.mirror(self.ttl3)
                delay_mu(987+31*d)
                self.mirror(self.ttl2)
                delay_mu(38132+d)
                self.mirror(self.ttl3)
                delay(1*us)
                self.urukul0_ch0.sw.off()
                delay(10*ms)
                self.print_async(d)
                # print_binary(spow, uint64, 64, print_bits=False)
                # print_binary(0x7fffffff, print_bits=True)
                # print_binary(self.frequency_to_uint32(300*MHz), print_bits=False)
                self.core.wait_until_mu(now_mu())
> **Migrated from GitHub:** [#2776](https://github.com/m-labs/artiq/issues/2776) --- # Bug Report ```python delay_mu(1) t_io_update_mu = now_mu() + T_write64_mu + io_update_delay_mu ``` makes Urukul AD9910 output this: ![delay_mu_1_first.png](https://github.com/user-attachments/assets/1381dc24-07ab-41af-9ff3-93b2390f1aae) -------------------------------------- ```python t_io_update_mu = now_mu() + T_write64_mu + io_update_delay_mu delay_mu(1) ``` makes Urukul AD9910 output this: ![now_mu_first.png](https://github.com/user-attachments/assets/1bad24f4-eba9-4463-9968-60f2659bd191) By the way: `self.urukul0_ch0.tune_io_update_delay()` and `print(self.urukul0_ch0.sync_data.io_update_delay)` both return `0`. ### Expected Behavior Calling `now_mu()` does not affect AD9910 output under any circumstances. ### Actual (undesired) Behavior AD9910 output is clearly affected by call to `now_mu()`. ### Background info - I want to switch an Urukul AD9910 from a frequency (say 200 MHz) to its mirror frequency (800 MHz) without a phase discontinuity. - I have been receiving help in this endeavor in [AD9910: Mirror frequencies invert sign of phase accumulation](https://forum.m-labs.hk/d/974-ad9910-mirror-frequencies-invert-sign-of-phase-accumulation/4). - I thought I had figured everything out, but then I ran into the above bug. - My goal is to keep track of an AD9910's phase with nanosecond precision, so yes `delay_mu(1)` makes a huge difference. ### My System * Host: Ubuntu Server 22.04 LTS; artiq commit [d7a380db01b05ef7b5d372584ab2f3568ea3b777](https://github.com/m-labs/artiq/commit/d7a380db01b05ef7b5d372584ab2f3568ea3b777) from 2025 May 16. * 1x Kasli SoC v1.1.1: artiq-zynq commit [7df7335cce41bb2d52832c25f8af5be80f9744ab](https://git.m-labs.hk/M-Labs/artiq-zynq/commit/7df7335cce41bb2d52832c25f8af5be80f9744ab) from 2025 May 21. * 1x Urukul v1.5.4: urukul-pld commit [50d24c0e69791efc2a0f588980d151c3964a5283](https://git.m-labs.hk/M-Labs/urukul-pld/commit/50d24c0e69791efc2a0f588980d151c3964a5283) from 2025 April 9. * 1x DIO_SMA v1.4 ### Experiment code that reproduces the bug Trigger your oscilloscope on falling edge of `self.ttl0` and set normal trigger mode and time span 100 ns. ```python from artiq.language.environment import EnvExperiment from artiq.language.core import kernel, rpc, delay, delay_mu, now_mu, at_mu, parallel from artiq.language.units import ns, us, ms, s, Hz, MHz, V from artiq.language.types import TInt32, TInt64, TFloat, TStr, TBool, TList, TTuple from artiq.coredevice.i2c import i2c_write_byte from artiq.coredevice.kasli_i2c import port_mapping from artiq.coredevice.ad9910 import _PHASE_MODE_DEFAULT, PHASE_MODE_CONTINUOUS, PHASE_MODE_ABSOLUTE, PHASE_MODE_TRACKING, _AD9910_REG_PROFILE0 from artiq.coredevice.urukul import DEFAULT_PROFILE from numpy import int32, uint32, int64, uint64 # Maps Kasli EEM port indices that are visible on the PCB # to actual electrical port(?) indices that need to be passed to the FPGA. KASLI_I2C_BOARD_TO_PORT_MAPPING = [port%8 for port in port_mapping.values()] # for `artiq.coredevice.i2c.i2c_write_byte(busno, busaddr, data, ack=True)` # and `artiq.coredevice.i2c.i2c_read_byte(busno, busaddr)` DIO_SMA_BUS_NUMBER = 0 DIO_SMA_BUS_ADDRESS = 0x7c # = 124 (decimal) or 01111100 (binary) @rpc def print_binary(number, type_cast=uint32, nr_bits=32, print_bits=False): print("binary :", f"{type_cast(number):{int(nr_bits)}b}") if print_bits: bits = "" for i in range(nr_bits): bits = str(i % 10) + bits print("bits :", bits) class DRGAmplitudeTest(EnvExperiment): def build(self): self.setattr_device("core") # artiq.coredevice.core.Core self.setattr_device("core_cache") # artiq.coredevice.cache.CoreCache device_db = self.get_device_db() # dict, DO NOT EDIT! self.n_kasli_socs = 1 + len(device_db["core"]["arguments"]["satellite_cpu_targets"]) self.setattr_device("i2c_switch0") # artiq.coredevice.i2c.I2CSwitch self.setattr_device("ttl0") # artiq.coredevice.ttl.TTLInOut self.setattr_device("ttl1") # artiq.coredevice.ttl.TTLInOut self.setattr_device("ttl2") # artiq.coredevice.ttl.TTLInOut self.setattr_device("ttl3") # artiq.coredevice.ttl.TTLInOut self.setattr_device("urukul0_cpld") # artiq.coredevice.urukul.CPLD self.setattr_device("urukul0_ch0") # artiq.coredevice.ad9910.AD9910 self.urukul0_ch0.ftw = int32(0) self.urukul0_ch0.pow = int32(0) self.urukul0_ch0.asf = int32(0) self.urukul0_ch0.t_acc_start_mu = int64(0) self.urukul0_ch0.acc_pow = int64(0) @kernel def init(self): r""" Should be called once after every reboot or power-cycle of the Kasli (SoC). """ for i in range(self.n_kasli_socs): while not self.core.get_rtio_destination_status(i): pass self.core.reset() self.core.break_realtime() self.i2c_switch0.set(channel = KASLI_I2C_BOARD_TO_PORT_MAPPING[0]) i2c_write_byte( busno = DIO_SMA_BUS_NUMBER, busaddr = DIO_SMA_BUS_ADDRESS, data = 0 ) self.i2c_switch0.unset() self.core.break_realtime() for ttl in [self.ttl0, self.ttl1, self.ttl2, self.ttl3]: ttl.output() delay(1*us) ttl.off() delay(1*us) self.urukul0_cpld.init() self.urukul0_cpld.cfg_att_en_all(1) self.urukul0_ch0.sw.off() self.urukul0_ch0.init() self.urukul0_ch0.set_phase_mode(PHASE_MODE_CONTINUOUS) self.urukul0_ch0.set_att(0.0) self.core.wait_until_mu(now_mu()) @kernel def frequency_to_uint32(self, frequency: TFloat) -> TInt32: """ Linearly map frequency ∈ [0*GHz, 1*GHz] to an unsigned 32-bit integer {0,1,..., 2**32-1}. Hacking is necessary because the ARTIQ compiler does *not* know unsigned integers. :param frequency: Must be in the interval [0*GHz, 1*GHz]. """ if frequency < 0*Hz: raise ValueError("Invalid AD9910 frequency!") elif frequency < self.urukul0_ch0.sysclk / 2: return self.urukul0_ch0.frequency_to_ftw(frequency) elif frequency <= self.urukul0_ch0.sysclk: return -1 - self.urukul0_ch0.frequency_to_ftw(self.urukul0_ch0.sysclk - frequency) else: raise ValueError("Invalid AD9910 frequency!") return int32(0) # prevents compiler crash @kernel def set_mu(self, ftw: TInt32, pow_: TInt32, asf: TInt32, phase_mode: TInt32 = _PHASE_MODE_DEFAULT, ref_time_mu: TInt64 = int64(-1), profile: TInt32 = DEFAULT_PROFILE) -> TInt32: if phase_mode == _PHASE_MODE_DEFAULT: phase_mode = self.urukul0_ch0.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.urukul0_ch0.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.urukul0_ch0.sysclk_per_mu >> 16 self.urukul0_ch0.write64(_AD9910_REG_PROFILE0 + profile, (asf << 16) | (pow_ & 0xffff), ftw) delay_mu(int64(self.urukul0_ch0.sync_data.io_update_delay)) t_io_update_mu = now_mu() self.urukul0_ch0.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: # phase accumulator has been reset self.urukul0_ch0.acc_pow = 0 self.urukul0_ch0.set_cfr1() # future IO_UPDATE will activate else: # calculate phase-offset word in AD9910's phase accumulator at rising flank of io_update dt_mu = t_io_update_mu - self.urukul0_ch0.t_acc_start_mu if self.urukul0_ch0.ftw >= 0: # regular frequency, so AD9910 has been incrementing phase accumulator self.urukul0_ch0.acc_pow += self.urukul0_ch0.ftw * dt_mu * self.urukul0_ch0.sysclk_per_mu else: # mirror frequency, so AD9910 has been decrementing phase accumulator # f_mirror + f = 1*GHz means that we just flip all of ftw's bits self.urukul0_ch0.acc_pow -= (~self.urukul0_ch0.ftw) * dt_mu * self.urukul0_ch0.sysclk_per_mu self.urukul0_ch0.t_acc_start_mu = t_io_update_mu self.urukul0_ch0.ftw = ftw self.urukul0_ch0.pow = pow_ self.urukul0_ch0.asf = asf return pow_ @kernel def mirror(self, debug_ttl): # align to coarse RTIO which aligns SYNC_CLK at_mu(now_mu() & ~7) # delay by 1 nanosecond, otherwise phase discontinuity # see also https://github.com/m-labs/artiq/issues/2776 # pre-calculate RTIO timeline cursor at next rising flank of io_update T_write64_mu = 1248 # RTIO timeline cursor advancement per register-write io_update_delay_mu = int64(self.urukul0_ch0.sync_data.io_update_delay) t_io_update_mu = now_mu() + T_write64_mu + io_update_delay_mu delay_mu(1) # f_mirror + f = 1*GHz means that we just flip all of ftw's bits ftw_mirror = ~self.urukul0_ch0.ftw # pre-calculate phase-offset word of AD9910's phase accumulator at next rising flank of io_update dt_mu = t_io_update_mu - self.urukul0_ch0.t_acc_start_mu if self.urukul0_ch0.ftw >= 0: # regular frequency, so AD9910 has been incrementing phase accumulator self.urukul0_ch0.acc_pow += self.urukul0_ch0.ftw * dt_mu * self.urukul0_ch0.sysclk_per_mu else: # mirror frequency, so AD9910 has been decrementing phase accumulator self.urukul0_ch0.acc_pow -= ftw_mirror * dt_mu * self.urukul0_ch0.sysclk_per_mu # pre-calculate phase-offset word of AD9910's total output phase at next rising flank of io_update pow_io_update_mu = (self.urukul0_ch0.acc_pow >> 16) + self.urukul0_ch0.pow # mirror phase-offset word around a multiple of 2π self.urukul0_ch0.pow -= 2*pow_io_update_mu # write mirror frequency to single-tone register self.urukul0_ch0.ftw = ftw_mirror self.urukul0_ch0.write64(_AD9910_REG_PROFILE0 + 7, (self.urukul0_ch0.asf << 16) | (self.urukul0_ch0.pow & 0xffff), self.urukul0_ch0.ftw) delay_mu(io_update_delay_mu) # record new switching time self.urukul0_ch0.t_acc_start_mu = now_mu() # transfer mirror frequency to active output register self.urukul0_ch0.io_update.pulse_mu(8) delay_mu(84) debug_ttl.off() delay_mu(-84) # verify that we pre-calculated the accumulated phase correctly # if self.urukul0_ch0.t_acc_start_mu != t_io_update_mu: # raise ValueError("You pre-calculated the RTIO time cursor wrongly, \ # so you caused a phase discontinuity. Check on scope!") @kernel def dds_set(self, frequ: TFloat, turns: TFloat, amp: TFloat, phase_mode: TInt32 = _PHASE_MODE_DEFAULT, ref_time_mu: TInt64 = int64(-1)): self.set_mu(self.frequency_to_uint32(frequ), self.urukul0_ch0.turns_to_pow(turns), self.urukul0_ch0.amplitude_to_asf(amp), phase_mode, ref_time_mu) @rpc(flags={"async"}) def print_async(self, d): print("d =", d) @kernel def run(self): self.init() self.core.reset() self.core.break_realtime() for d in range(-20, 20+1, 1): for p in range(1): delay(50*ms) self.dds_set(100*MHz, 0.0, 0.1, PHASE_MODE_ABSOLUTE) delay(20*us) self.urukul0_ch0.sw.on() delay(151*ns) self.ttl0.on() self.ttl1.on() self.ttl2.on() delay(-1*us + d*ns) self.mirror(self.ttl0) delay_mu(434+8-5*d) self.mirror(self.ttl3) delay_mu(4321+2*d) self.mirror(self.ttl3) delay_mu(459+4*d) self.mirror(self.ttl3) delay_mu(3456+11*d) self.mirror(self.ttl3) # delay(2*s) delay_mu(2349+1*d) self.mirror(self.ttl3) delay_mu(2541-9*d) self.mirror(self.ttl3) delay_mu(984-4*d) self.mirror(self.ttl1) delay_mu(12345+3*d) self.mirror(self.ttl3) delay_mu(491+7*d) self.mirror(self.ttl3) delay_mu(42459-8*d) self.mirror(self.ttl3) delay_mu(987+31*d) self.mirror(self.ttl2) delay_mu(38132+d) self.mirror(self.ttl3) delay(1*us) self.urukul0_ch0.sw.off() delay(10*ms) self.print_async(d) # print_binary(spow, uint64, 64, print_bits=False) # print_binary(0x7fffffff, print_bits=True) # print_binary(self.frequency_to_uint32(300*MHz), print_bits=False) self.core.wait_until_mu(now_mu()) ```
sb10q closed this issue 2026-01-18 19:06:15 +08:00
Contributor

Why do you say that "AD9910 output is clearly affected by call to now_mu()"? From the short description, what seems to be going on is that shifting the IO_UPDATE edge by 1 ns has an effect (the delay being either taken into account in the calculation or not). This is completely expected, as for deterministic phase control, the IO_UPDATE pulse must be aligned with respect to the 250 MHz internal DDS logic clock (assuming you are running with 1 GHz SYSCLK). io_update_delay (0, 1, 2 or 3) specifies the alignment with the 4 ns grid, i.e. it is not related to the an output delay of any kind, but specifies one of the four possible grids w.r.t. the 1 ns timeline resolution that is sure to meet the setup/hold requirements on the DDS end.

Why do you say that "AD9910 output is clearly affected by call to now_mu()"? From the short description, what seems to be going on is that shifting the IO_UPDATE edge by 1 ns has an effect (the delay being either taken into account in the calculation or not). This is completely expected, as for deterministic phase control, the IO_UPDATE pulse must be aligned with respect to the 250 MHz internal DDS logic clock (assuming you are running with 1 GHz SYSCLK). `io_update_delay` (0, 1, 2 or 3) specifies the alignment with the 4 ns grid, i.e. it is not related to the an output delay of any kind, but specifies one of the four possible grids w.r.t. the 1 ns timeline resolution that is sure to meet the setup/hold requirements on the DDS end.
Author

@dnadlinger Thank you, you are absolutely right. It was late and I wasn't reasoning clearly.

Incidentally, do you know a good way to align io_update to the SYNC_CLK grid? self.urukul0_ch0.tune_io_update_delay() and print(self.urukul0_ch0.sync_data.io_update_delay) both return 0, so I would expect everything to be fine, but then I observe a dependence of the optimal alignment delay on the frequency tuning word, see my latest answer to "AD9910: Mirror frequencies invert sign of phase accumulation".

@dnadlinger Thank you, you are absolutely right. It was late and I wasn't reasoning clearly. Incidentally, do you know a good way to align io_update to the SYNC_CLK grid? `self.urukul0_ch0.tune_io_update_delay()` and `print(self.urukul0_ch0.sync_data.io_update_delay)` both return 0, so I would expect everything to be fine, but then I observe a dependence of the optimal alignment delay on the frequency tuning word, see [my latest answer to "AD9910: Mirror frequencies invert sign of phase accumulation"](https://forum.m-labs.hk/d/974-ad9910-mirror-frequencies-invert-sign-of-phase-accumulation/9).
Contributor

tune_io_update_delay() is I think Robert Jördens's innovation when he integrated the multi-chip sync/phase calculations properly into upstream ARTIQ. We originally just mapped the four possible values manually when bringing up phase-coherent operation in Oxford, but I don't think I've ever seen the auto-tuning implementation fail. (If memory serves, it even uses the DRG to figure out the correct alignment, so one would hope it would definitely yield the correct result for your use case?)

`tune_io_update_delay()` is I think Robert Jördens's innovation when he integrated the multi-chip sync/phase calculations properly into upstream ARTIQ. We originally just mapped the four possible values manually when bringing up phase-coherent operation in Oxford, but I don't think I've ever seen the auto-tuning implementation fail. (If memory serves, it even uses the DRG to figure out the correct alignment, so one would hope it would definitely yield the correct result for your use case?)
Sign in to join this conversation.