The following experiment mirrors the frequency of self.urukul0_ch0
thirteen times with random delays in between completely *without* phase discontinuities. To observe the output, trigger your oscilloscope on falling edge of either self.ttl0
or self.ttl1
or self.ttl2
and set normal trigger mode and time span 100 ns.
Beware that phase discontinuities *do* occur if one introduces a delay > 2 seconds (roughly) between any of the frequency mirrorings. The most likely cause is improper handling of integer overflows / underflows during multiplication.
@occheung Do you have any advice how to handle the overflows / underflows during multiplication?
Correction from later: The experiment has no phase discontinuities for pretty frequencies like 100 or 200 MHz. For the random frequency 238.3486 MHz, every single mirroring causes a phase discontinuity. The discontinuities become smaller if I change delay_mu(1)
to delay_mu(0)
and they almost disappear for delay_mu(2)
. Clearly, the required delay between at_mu(now_mu() & ~7)
and t_io_update_mu = now_mu() + [...]
depends on the frequency. I am close to giving up and doing falling frequency ramps the exact same way as falling amplitude ramps, i.e. with a 4 ns-long discontinuity in the ramped parameter.
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
delay_mu(1)
# 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
# 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())