"""RTIO driver for the Analog Devices ADF[45]35[56] family of GHz PLLs
on Mirny-style prefixed SPI buses.
"""
# https://github.com/analogdevicesinc/linux/blob/master/Documentation/devicetree/bindings/iio/frequency/adf5355.txt
# https://github.com/analogdevicesinc/linux/blob/master/drivers/iio/frequency/adf5355.c
# https://www.analog.com/media/en/technical-documentation/data-sheets/ADF5355.pdf
# https://www.analog.com/media/en/technical-documentation/data-sheets/ADF5355.pdf
# https://www.analog.com/media/en/technical-documentation/user-guides/EV-ADF5355SD1Z-UG-1087.pdf
from numpy import int32, int64
from math import floor, ceil
from artiq.language.core import compile, Kernel, KernelInvariant, kernel, portable, round64
from artiq.language.units import us, GHz, MHz
from artiq.coredevice.core import Core
from artiq.coredevice.mirny import Mirny
from artiq.coredevice.ttl import TTLOut
from artiq.coredevice.spi2 import *
from artiq.coredevice.adf5356_reg import *
SPI_CONFIG = (
0 * SPI_OFFLINE
| 0 * SPI_END
| 0 * SPI_INPUT
| 1 * SPI_CS_POLARITY
| 0 * SPI_CLK_POLARITY
| 0 * SPI_CLK_PHASE
| 0 * SPI_LSB_FIRST
| 0 * SPI_HALF_DUPLEX
)
ADF5356_MIN_VCO_FREQ = 3.4 * GHz
ADF5356_MAX_VCO_FREQ = 6.8 * GHz
ADF5356_MAX_FREQ_PFD = int32(125.0 * MHz)
ADF5356_MODULUS1 = int32(1 << 24)
ADF5356_MAX_MODULUS2 = int32(1 << 28) # FIXME: ADF5356 has 28 bits MOD2
ADF5356_MAX_R_CNT = int32(1023)
[docs]
@compile
class ADF5356:
"""Analog Devices AD[45]35[56] family of GHz PLLs.
:param cpld_device: Mirny CPLD device name
:param sw_device: Mirny RF switch device name
:param channel: Mirny RF channel index
:param ref_doubler: enable/disable reference clock doubler
:param ref_divider: enable/disable reference clock divide-by-2
:param core_device: Core device name (default: "core")
"""
core: KernelInvariant[Core]
cpld: KernelInvariant[Mirny]
channel: KernelInvariant[int32]
sw: KernelInvariant[TTLOut]
sysclk: KernelInvariant[float]
regs: Kernel[list[int32]]
ref_doubler: Kernel[bool]
ref_divider: Kernel[bool]
def __init__(
self,
dmgr,
cpld_device,
sw_device,
channel,
ref_doubler=False,
ref_divider=False,
core="core",
):
self.cpld = dmgr.get(cpld_device)
self.sw = dmgr.get(sw_device)
self.channel = channel
self.core = dmgr.get(core)
self.ref_doubler = ref_doubler
self.ref_divider = ref_divider
self.sysclk = self.cpld.refclk
assert 10 <= self.sysclk / 1e6 <= 600
self._init_registers()
@staticmethod
def get_rtio_channels(**kwargs):
return []
[docs]
@kernel
def init(self, blind: bool = False):
"""
Initialize and configure the PLL.
:param blind: Do not attempt to verify presence.
"""
self.sync()
if not blind:
# MUXOUT = VDD
self.regs[4] = ADF5356_REG4_MUXOUT_UPDATE(self.regs[4], 1)
self.write(self.regs[4])
self.core.delay(1000. * us)
if not self.read_muxout():
raise ValueError("MUXOUT not high")
self.core.delay(800. * us)
# MUXOUT = DGND
self.regs[4] = ADF5356_REG4_MUXOUT_UPDATE(self.regs[4], 2)
self.write(self.regs[4])
self.core.delay(1000. * us)
if self.read_muxout():
raise ValueError("MUXOUT not low")
self.core.delay(800. * us)
# MUXOUT = digital lock-detect
self.regs[4] = ADF5356_REG4_MUXOUT_UPDATE(self.regs[4], 6)
self.write(self.regs[4])
[docs]
@kernel
def set_att(self, att: float):
"""Set digital step attenuator in SI units.
This method will write the attenuator settings of the channel.
See also :meth:`Mirny.set_att<artiq.coredevice.mirny.Mirny.set_att>`.
:param att: Attenuation in dB.
"""
self.cpld.set_att(self.channel, att)
[docs]
@kernel
def set_att_mu(self, att: int32):
"""Set digital step attenuator in machine units.
:param att: Attenuation setting, 8-bit digital.
"""
self.cpld.set_att_mu(self.channel, att)
[docs]
@kernel
def write(self, data: int32):
"""
Write to the input shift register.
Data transfer from the input shift register to a data register. The 4
LSBs are control bits, which determines the destination register.
:param data: Data to be written
"""
self.cpld.write_ext(self.channel | 4, 32, data)
[docs]
@kernel
def read_muxout(self) -> bool:
"""
Read the state of the MUXOUT line.
By default, this is configured to be the digital lock detection.
"""
return bool(self.cpld.read_reg(0) & (1 << (self.channel + 8)))
[docs]
@kernel
def set_output_power_mu(self, n: int32):
"""
Set the power level at output A of the PLL chip in machine units.
This driver defaults to `n = 3` at init.
:param n: output power setting, 0, 1, 2, or 3 (see ADF5356 datasheet, fig. 44).
"""
if not 0 <= n <= 3:
raise ValueError("invalid power setting")
self.regs[6] = ADF5356_REG6_RF_OUTPUT_A_POWER_UPDATE(self.regs[6], n)
self.write(self.regs[6])
[docs]
@portable
def output_power_mu(self) -> int32:
"""
Return the power level at output A of the PLL chip in machine units.
"""
return ADF5356_REG6_RF_OUTPUT_A_POWER_GET(self.regs[6])
[docs]
@kernel
def enable_output(self):
"""
Enable output A of the PLL chip. This is the default after init.
"""
self.regs[6] |= ADF5356_REG6_RF_OUTPUT_A_ENABLE(1)
self.write(self.regs[6])
[docs]
@kernel
def disable_output(self):
"""
Disable output A of the PLL chip.
"""
self.regs[6] &= ~ADF5356_REG6_RF_OUTPUT_A_ENABLE(1)
self.write(self.regs[6])
[docs]
@kernel
def set_frequency(self, f: float):
"""
Output given frequency on output A.
:param frequency: 53.125 MHz <= f <= 6800 MHz
"""
if f > ADF5356_MAX_VCO_FREQ:
raise ValueError("Requested too high frequency")
# select minimal output divider
rf_div_sel = 0
while f < ADF5356_MIN_VCO_FREQ:
f *= 2.0
rf_div_sel += 1
if (1 << rf_div_sel) > 64:
raise ValueError("Requested too low frequency")
# choose reference divider that maximizes PFD frequency
self.regs[4] = ADF5356_REG4_R_COUNTER_UPDATE(
self.regs[4], self._compute_reference_counter()
)
f_pfd = self.f_pfd()
# choose prescaler
if f > 6. * GHz: # see datasheet "Prescaler Value" section (Rev. A, p.21)
self.regs[0] |= ADF5356_REG0_PRESCALER(1) # 8/9
n_min, n_max = 75, 65535
# adjust reference divider to be able to match n_min constraint
while int64(n_min) * f_pfd > int64(f):
r = ADF5356_REG4_R_COUNTER_GET(self.regs[4])
self.regs[4] = ADF5356_REG4_R_COUNTER_UPDATE(self.regs[4], r + 1)
f_pfd = self.f_pfd()
else:
self.regs[0] &= ~ADF5356_REG0_PRESCALER(1) # 4/5
n_min, n_max = 23, 32767
# calculate PLL parameters
n, frac1, (frac2_msb, frac2_lsb), (mod2_msb, mod2_lsb) = calculate_pll(
f, f_pfd
)
if not (n_min <= n <= n_max):
raise ValueError("Invalid INT value")
# configure PLL
self.regs[0] = ADF5356_REG0_INT_VALUE_UPDATE(self.regs[0], n)
self.regs[1] = ADF5356_REG1_MAIN_FRAC_VALUE_UPDATE(self.regs[1], frac1)
self.regs[2] = ADF5356_REG2_AUX_FRAC_LSB_VALUE_UPDATE(self.regs[2], frac2_lsb)
self.regs[2] = ADF5356_REG2_AUX_MOD_LSB_VALUE_UPDATE(self.regs[2], mod2_lsb)
self.regs[13] = ADF5356_REG13_AUX_FRAC_MSB_VALUE_UPDATE(
self.regs[13], frac2_msb
)
self.regs[13] = ADF5356_REG13_AUX_MOD_MSB_VALUE_UPDATE(self.regs[13], mod2_msb)
self.regs[6] = ADF5356_REG6_RF_DIVIDER_SELECT_UPDATE(self.regs[6], rf_div_sel)
self.regs[6] = ADF5356_REG6_NEGATIVE_BLEED_UPDATE(
self.regs[6],
self._compute_negative_bleed_condition()
)
self.regs[6] = ADF5356_REG6_CP_BLEED_CURRENT_UPDATE(
self.regs[6], floor(24. * float(f_pfd) / (61.44 * MHz))
)
self.regs[9] = ADF5356_REG9_VCO_BAND_DIVISION_UPDATE(
self.regs[9], ceil(float(f_pfd) / 1600e3)
)
self._update_vco_timeout(f_pfd)
self.regs[10] = ADF5356_REG10_ADC_CLK_DIV_UPDATE(
self.regs[10], min(ceil(((float(f_pfd) / 100e3) - 2.) / 4.), 255)
)
# commit
self.sync()
[docs]
@kernel
def sync(self):
"""
Write all registers to the device. Attempts to lock the PLL.
"""
f_pfd = self.f_pfd()
self.core.delay(200. * us) # Slack
if f_pfd <= round64(75.0 * MHz):
for i in range(13, 0, -1):
self.write(self.regs[i])
self.core.delay(200. * us)
self.write(self.regs[0] | ADF5356_REG0_AUTOCAL(1))
else:
# AUTOCAL AT HALF PFD FREQUENCY
# calculate PLL at f_pfd/2
n, frac1, (frac2_msb, frac2_lsb), (mod2_msb, mod2_lsb) = calculate_pll(
self.f_vco(), f_pfd >> 1
)
self.core.delay(200. * us) # Slack
self.write(
13
| ADF5356_REG13_AUX_FRAC_MSB_VALUE(frac2_msb)
| ADF5356_REG13_AUX_MOD_MSB_VALUE(mod2_msb)
)
for i in range(12, 4, -1):
self.write(self.regs[i])
self.write(
ADF5356_REG4_R_COUNTER_UPDATE(self.regs[4], 2 * self.ref_counter())
)
self.write(self.regs[3])
self.write(
2
| ADF5356_REG2_AUX_MOD_LSB_VALUE(mod2_lsb)
| ADF5356_REG2_AUX_FRAC_LSB_VALUE(frac2_lsb)
)
self.write(1 | ADF5356_REG1_MAIN_FRAC_VALUE(frac1))
self.core.delay(200. * us)
self.write(ADF5356_REG0_INT_VALUE(n) | ADF5356_REG0_AUTOCAL(1))
# RELOCK AT WANTED PFD FREQUENCY
for i in [13, 4, 2, 1]:
self.write(self.regs[i])
# force-disable autocal
self.write(self.regs[0] & ~ADF5356_REG0_AUTOCAL(1))
[docs]
@portable
def f_pfd(self) -> int64:
"""
Return the PFD frequency for the cached set of registers.
"""
r = ADF5356_REG4_R_COUNTER_GET(self.regs[4])
d = ADF5356_REG4_R_DOUBLER_GET(self.regs[4])
t = ADF5356_REG4_R_DIVIDER_GET(self.regs[4])
return self._compute_pfd_frequency(r, d, t)
[docs]
@portable
def f_vco(self) -> float:
"""
Return the VCO frequency for the cached set of registers.
"""
return float(self.f_pfd()) * (
float(self.pll_n())
+ (
float(self.pll_frac1())
+ self.pll_frac2() / self.pll_mod2()
)
/ float(ADF5356_MODULUS1)
)
[docs]
@portable
def pll_n(self) -> int32:
"""
Return the PLL integer value (INT) for the cached set of registers.
"""
return ADF5356_REG0_INT_VALUE_GET(self.regs[0])
[docs]
@portable
def pll_frac1(self) -> int32:
"""
Return the main fractional value (FRAC1) for the cached set of registers.
"""
return ADF5356_REG1_MAIN_FRAC_VALUE_GET(self.regs[1])
[docs]
@portable
def pll_frac2(self) -> int32:
"""
Return the auxiliary fractional value (FRAC2) for the cached set of registers.
"""
return (
ADF5356_REG13_AUX_FRAC_MSB_VALUE_GET(self.regs[13]) << 14
) | ADF5356_REG2_AUX_FRAC_LSB_VALUE_GET(self.regs[2])
[docs]
@portable
def pll_mod2(self) -> int32:
"""
Return the auxiliary modulus value (MOD2) for the cached set of registers.
"""
return (
ADF5356_REG13_AUX_MOD_MSB_VALUE_GET(self.regs[13]) << 14
) | ADF5356_REG2_AUX_MOD_LSB_VALUE_GET(self.regs[2])
[docs]
@portable
def ref_counter(self) -> int32:
"""
Return the reference counter value (R) for the cached set of registers.
"""
return ADF5356_REG4_R_COUNTER_GET(self.regs[4])
[docs]
@portable
def output_divider(self) -> int32:
"""
Return the value of the output A divider.
"""
return 1 << ADF5356_REG6_RF_DIVIDER_SELECT_GET(self.regs[6])
[docs]
def info(self):
"""
Return a summary of high-level parameters as a dict.
"""
prescaler = ADF5356_REG0_PRESCALER_GET(self.regs[0])
return {
# output
"f_outA": self.f_vco() / self.output_divider(),
"f_outB": self.f_vco() * 2,
"output_divider": self.output_divider(),
# PLL parameters
"f_vco": self.f_vco(),
"pll_n": self.pll_n(),
"pll_frac1": self.pll_frac1(),
"pll_frac2": self.pll_frac2(),
"pll_mod2": self.pll_mod2(),
"prescaler": "4/5" if prescaler == 0 else "8/9",
# reference / PFD
"sysclk": self.sysclk,
"ref_doubler": self.ref_doubler,
"ref_divider": self.ref_divider,
"ref_counter": self.ref_counter(),
"f_pfd": self.f_pfd(),
}
@portable
def _init_registers(self):
"""
Initialize cached registers with sensible defaults.
"""
# fill with control bits
self.regs = [int32(i) for i in range(ADF5356_NUM_REGS)]
# REG2
# ====
# avoid divide-by-zero
self.regs[2] |= ADF5356_REG2_AUX_MOD_LSB_VALUE(1)
# REG4
# ====
# single-ended reference mode is recommended
# for references up to 250 MHz, even if the signal is differential
if self.sysclk <= 250.*MHz:
self.regs[4] |= ADF5356_REG4_REF_MODE(0)
else:
self.regs[4] |= ADF5356_REG4_REF_MODE(1)
# phase detector polarity: positive
self.regs[4] |= ADF5356_REG4_PD_POLARITY(1)
# charge pump current: 0.94 mA
self.regs[4] |= ADF5356_REG4_CURRENT_SETTING(2)
# MUXOUT: digital lock detect
self.regs[4] |= ADF5356_REG4_MUX_LOGIC(1) # 3v3 logic
self.regs[4] |= ADF5356_REG4_MUXOUT(6)
# setup reference path
if self.ref_doubler:
self.regs[4] |= ADF5356_REG4_R_DOUBLER(1)
if self.ref_divider:
self.regs[4] |= ADF5356_REG4_R_DIVIDER(1)
r = self._compute_reference_counter()
self.regs[4] |= ADF5356_REG4_R_COUNTER(r)
# REG5
# ====
# reserved values
self.regs[5] = int32(0x800025)
# REG6
# ====
# reserved values
self.regs[6] = int32(0x14000006)
# configure negative bleed
self.regs[6] |= ADF5356_REG6_NEGATIVE_BLEED(
self._compute_negative_bleed_condition()
)
# charge pump bleed current
self.regs[6] |= ADF5356_REG6_CP_BLEED_CURRENT(
floor(24. * float(self.f_pfd()) / (61.44 * MHz))
)
# direct feedback from VCO to N counter
self.regs[6] |= ADF5356_REG6_FB_SELECT(1)
# mute until the PLL is locked
self.regs[6] |= ADF5356_REG6_MUTE_TILL_LD(1)
# enable output A
self.regs[6] |= ADF5356_REG6_RF_OUTPUT_A_ENABLE(1)
# set output A power to max power, is adjusted by extra attenuator
self.regs[6] |= ADF5356_REG6_RF_OUTPUT_A_POWER(3) # +5 dBm
# REG7
# ====
# reserved values
self.regs[7] = int32(0x4000007)
# sync load-enable to reference
self.regs[7] |= ADF5356_REG7_LE_SYNC(1)
# frac-N lock-detect precision: 12 ns
self.regs[7] |= ADF5356_REG7_FRAC_N_LD_PRECISION(3)
# REG8
# ====
# reserved values
self.regs[8] = int32(0x15596568)
# REG9
# ====
# default timeouts (from eval software)
self.regs[9] |= (
ADF5356_REG9_SYNTH_LOCK_TIMEOUT(13)
| ADF5356_REG9_AUTOCAL_TIMEOUT(31)
)
f_pfd = self.f_pfd()
self._update_vco_timeout(f_pfd)
self.regs[9] |= ADF5356_REG9_VCO_BAND_DIVISION(
ceil(float(f_pfd) / 1600e3)
)
# REG10
# =====
# reserved values
self.regs[10] = int32(0xC0000A)
# ADC defaults (from eval software)
self.regs[10] |= (
ADF5356_REG10_ADC_ENABLE(1)
# Equation (15)
| ADF5356_REG10_ADC_CLK_DIV(
min(ceil(((f_pfd / int64(100e3)) - 2.) / 4.), 255)
)
| ADF5356_REG10_ADC_CONV(1)
)
# REG11
# =====
# reserved values
self.regs[11] = int32(0x61200B)
# REG12
# =====
# reserved values
self.regs[12] = int32(0x15FC)
@portable
def _compute_pfd_frequency(self, r: int32, d: int32, t: int32) -> int64:
"""
Calculate the PFD frequency from the given reference path parameters.
"""
return round64(self.sysclk * (float(1 + d) / float(r * (1 + t))))
@portable
def _compute_reference_counter(self) -> int32:
"""
Determine the reference counter R that maximizes the PFD frequency.
"""
d = ADF5356_REG4_R_DOUBLER_GET(self.regs[4])
t = ADF5356_REG4_R_DIVIDER_GET(self.regs[4])
r = 1
while self._compute_pfd_frequency(r, d, t) > int64(ADF5356_MAX_FREQ_PFD):
r += 1
return r
@portable
def _compute_negative_bleed_condition(self) -> int32:
"""
Determine if negative bleed should be enabled.
See the "Register Map, Register 6, Negative Bleed" documentation.
"""
return 1 if ((self.pll_frac1() == 0) and (self.pll_frac2() == 0)
and (float(self.f_pfd()) <= 100. * MHz)) else 0
@portable
def _update_vco_timeout(self, f_pfd: int64):
"""
Update the timeout value for the VCO band select.
See the "Register Map, Register 9" documentation.
"""
autocal_timeout = float(ADF5356_REG9_AUTOCAL_TIMEOUT_GET(self.regs[9]))
synth_lock_timeout = float(ADF5356_REG9_SYNTH_LOCK_TIMEOUT_GET(self.regs[9]))
vco_timeout = max(
# Inequality (13)
floor((50. * us * float(f_pfd) / autocal_timeout) + 1.),
# Inequality (14)
floor((20. * us * float(f_pfd) / synth_lock_timeout) + 1.)
)
self.regs[9] = ADF5356_REG9_TIMEOUT_UPDATE(self.regs[9], vco_timeout)
@portable
def gcd(a: int64, b: int64) -> int64:
while b != int64(0):
a, b = b, a % b
return a
@portable
def split_msb_lsb_28b(v: int32) -> tuple[int32, int32]:
return (v >> 14) & 0x3FFF, v & 0x3FFF
[docs]
@portable
def calculate_pll(f_vco: float, f_pfd: int64) -> tuple[int32, int32, tuple[int32, int32], tuple[int32, int32]]:
"""
Calculate fractional-N PLL parameters such that
``f_vco = f_pfd * (n + (frac1 + frac2/mod2) / mod1)``
where
``mod1 = 2**24`` and ``mod2 <= 2**28``
:param f_vco: target VCO frequency
:param f_pfd: PFD frequency
:return: (``n``, ``frac1``, ``(frac2_msb, frac2_lsb)``, ``(mod2_msb, mod2_lsb)``)
"""
f_pfd = int64(f_pfd)
# integral part
n, r = int32(f_vco // float(f_pfd)), f_vco % float(f_pfd)
# main fractional part
r *= float(ADF5356_MODULUS1)
frac1, frac2 = int32(r // float(f_pfd)), int64(r % float(f_pfd))
# auxiliary fractional part
mod2 = f_pfd
while mod2 > int64(ADF5356_MAX_MODULUS2):
mod2 >>= 1
frac2 >>= 1
gcd_div = gcd(frac2, mod2)
mod2 //= gcd_div
frac2 //= gcd_div
return n, frac1, split_msb_lsb_28b(int32(frac2)), split_msb_lsb_28b(int32(mod2))