@occheung Thank you for your advice! I didn't understand "autoclear causes a glitch", so I accidentally reproduced your findings from yesterday. Next, I will try switching to the mirror frequency without the autoclear. Purely for documentation purposes:
pow_
has a time resolution of oscillation_period/2**16
, i.e. 0.15 picoseconds at f=100 MHz or 0.3 picoseconds at f=200 MHz. However, now_mu()-ref_time_mu
only has a time resolution of 1 nanosecond, which is already 10% of one period at f=100 MHz or 20% of one period at f=200 MHz. For a good phase match before and after the switch to the mirror frequency, we need to add a constant p
to pow_
that cannot be calculated from now_mu()-ref_time_mu
.
- At the precise moment of the switch from
(100*MHz, 0.0, 0.1)
to (900*MHz, pow_, 0.1)
, i.e. at the rising flank of the io_update pulse, the AD9910 chip outputs bullshit for roughly 5 nanoseconds:

- I scanned
p
(and therefore pow_
) in the range (-2π, 2π), but the output always contained 5 nanoseconds of bullshit. See for yourself in this video.
- I introduced an additional delay
d
between the buffer write self.urukul0.ch0.write64(...)
and the transfer to the active output registers self.urukul0_ch0.io_update.pulse_mu(8)
and performed a fine 2-dimensional scan of d
and p
in the range (0, 40 ns) x (-2π, 2π), but the output always contained 5 nanoseconds of bullshit.
Run it yourself
The code below:
- Aligns the output phase of
self.urukul0_ch0
deterministically to the edges of self.ttl0
via self.dds_set(100*MHz, 0.0, 0.1, PHASE_MODE_ABSOLUTE)
.
- Switches to the mirror frequency via
self.dds_set(900*MHz, 0.0, 0.1, PHASE_MODE_TRACKING, self.urukul0_ch0.t_sw, p, d)
, where p
is the constant phase added to pow_
and d
is the aforementioned delay before io_update.
- Marks the switch to the mirror frequency via
self.ttl0.off()
.
- Repeats the above steps 800 times while:
- scanning
d
over (0, 40 ns) in 40 steps (outer loop),
- scanning
pow_
over (-2π, 2π) in 20 steps (inner loop),
- taking 21 ms per iteration for a total duration < 20 seconds.
To observe the output, trigger your scope on the falling edge of self.ttl0
and set it to normal trigger mode and 100 nanosecond time span. Also, make sure your scope is able to trigger every 21 ms or increase that duration.
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, 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,\
_AD9910_REG_RAMP_LIMIT, _AD9910_REG_RAMP_STEP, _AD9910_REG_RAMP_RATE
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(flags={"async"})
# def rpc_print(reg):
# for i, r in enumerate(reg):
# print(f"REG{i}: {r:64b} | decimal: {r}")
# print(f"bits: 3210987654321098765432109876543210987654321098765432109876543210 <-- LSB here")
@rpc
def print_binary(number, type_cast, nr_bits):
bits = ""
for i in range(nr_bits):
bits = str(i % 10) + bits
print("bits :", bits)
print("binary :", f"{type_cast(number):{int(nr_bits)}b}")
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("urukul0_cpld") # artiq.coredevice.urukul.CPLD
self.setattr_device("urukul0_ch0") # artiq.coredevice.ad9910.AD9910
self.urukul0_ch0.t_sw = 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])
delay(1*us)
i2c_write_byte(
busno = DIO_SMA_BUS_NUMBER,
busaddr = DIO_SMA_BUS_ADDRESS,
data = 0
)
delay(1*us)
self.i2c_switch0.unset()
self.core.break_realtime()
for ttl in [self.ttl0, self.ttl1, self.ttl2]:
ttl.output()
delay(1*us)
ttl.off()
delay(1*us)
self.urukul0_cpld.init()
delay(1*us)
self.urukul0_cpld.cfg_att_en_all(1)
delay(1*us)
self.urukul0_ch0.sw.off()
delay(1*us)
self.urukul0_ch0.init()
delay(1*us)
self.urukul0_ch0.set_phase_mode(PHASE_MODE_CONTINUOUS)
delay(1*us)
self.urukul0_ch0.set_att(0.0)
delay(1*us)
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,
ram_destination: TInt32 = -1,
p: TInt32 = 0, d: TInt64 = 0) -> 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() - ref_time_mu)
pow_ += (dt * ftw * self.urukul0_ch0.sysclk_per_mu >> 16) + 13000 + round(p/10 * (1 << 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))
delay_mu(d)
self.urukul0_ch0.t_sw = 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
delay(90*ns)
self.ttl0.off()
delay(-90*ns)
if phase_mode != PHASE_MODE_CONTINUOUS:
self.urukul0_ch0.set_cfr1()
# future IO_UPDATE will activate
return pow_
@kernel
def dds_set(self, frequ: TFloat, turns: TFloat, amp: TFloat,
phase_mode: TInt32 = _PHASE_MODE_DEFAULT,
ref_time_mu: TInt64 = int64(-1),
p: TInt32 = 0, d: TInt64 = 0):
# if phase_mode == PHASE_MODE_TRACKING:
# phase_mode = self.urukul0_ch0.phase_mode
ftw = self.frequency_to_uint32(frequ)
pow_ = self.urukul0_ch0.turns_to_pow(turns)
asf = self.urukul0_ch0.amplitude_to_asf(amp)
self.set_mu(ftw, pow_, asf, phase_mode, ref_time_mu, p=p, d=d)
self.core_cache.put("urukul0_ch0", [ftw, pow_, asf])
@rpc(flags={"async"})
def print_async(self, d, p):
print("delay =", d, "turns =", p/10)
@kernel
def run(self):
self.init()
self.core.reset()
self.core.break_realtime()
# ---------------------------------
# t0 = now_mu()
# self.urukul0_cpld.set_profile(0, 7)
# print(now_mu() - t0)
delay(100*ms)
for d in range(0, 40, 1):
for p in range(-10, 11, 1):
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()
delay(-1.8*us)
self.dds_set(900*MHz, 0.0, 0.1, PHASE_MODE_TRACKING, self.urukul0_ch0.t_sw, p, d)
delay(1*us)
self.urukul0_ch0.sw.off()
self.print_async(d, p)
delay(20*ms)
self.core.wait_until_mu(now_mu())
delay(1*ms)