Source code for artiq.coredevice.mirny

"""RTIO driver for Mirny (4 channel GHz PLLs)
"""

from artiq.language.core import kernel, delay, portable
from artiq.language.units import us

from numpy import int32

from artiq.coredevice import spi2 as spi


SPI_CONFIG = (
    0 * spi.SPI_OFFLINE
    | 0 * spi.SPI_END
    | 0 * spi.SPI_INPUT
    | 1 * spi.SPI_CS_POLARITY
    | 0 * spi.SPI_CLK_POLARITY
    | 0 * spi.SPI_CLK_PHASE
    | 0 * spi.SPI_LSB_FIRST
    | 0 * spi.SPI_HALF_DUPLEX
)

# SPI clock write and read dividers
SPIT_WR = 4
SPIT_RD = 16

SPI_CS = 1

WE = 1 << 24

# supported CPLD code version
PROTO_REV_MATCH = 0x0

# almazny-specific data
ALMAZNY_REG_BASE = 0x0C
ALMAZNY_OE_SHIFT = 12

# higher SPI write divider to match almazny shift register timing 
# min SER time before SRCLK rise = 125ns
# -> div=32 gives 125ns for data before clock rise
# works at faster dividers too but could be less reliable
ALMAZNY_SPIT_WR = 32


[docs]class Mirny: """ Mirny PLL-based RF generator. :param spi_device: SPI bus device :param refclk: Reference clock (SMA, MMCX or on-board 100 MHz oscillator) frequency in Hz :param clk_sel: Reference clock selection. Valid options are: "XO" - onboard crystal oscillator; "SMA" - front-panel SMA connector; "MMCX" - internal MMCX connector. Passing an integer writes it as ``clk_sel`` in the CPLD's register 1. The effect depends on the hardware revision. :param core_device: Core device name (default: "core") """ kernel_invariants = {"bus", "core", "refclk", "clk_sel_hw_rev"} def __init__(self, dmgr, spi_device, refclk=100e6, clk_sel="XO", core_device="core"): self.core = dmgr.get(core_device) self.bus = dmgr.get(spi_device) # reference clock frequency self.refclk = refclk if not (10 <= self.refclk / 1e6 <= 600): raise ValueError("Invalid refclk") # reference clock selection try: self.clk_sel_hw_rev = { # clk source: [reserved, reserved, v1.1, v1.0] "xo": [-1, -1, 0, 0], "mmcx": [-1, -1, 3, 2], "sma": [-1, -1, 2, 3], }[clk_sel.lower()] except AttributeError: # not a string, fallback to int if clk_sel & 0x3 != clk_sel: raise ValueError("Invalid clk_sel") from None self.clk_sel_hw_rev = [clk_sel] * 4 except KeyError: raise ValueError("Invalid clk_sel") from None self.clk_sel = -1 # board hardware revision self.hw_rev = 0 # v1.0: 3, v1.1: 2 # TODO: support clk_div on v1.0 boards
[docs] @kernel def read_reg(self, addr): """Read a register""" self.bus.set_config_mu( SPI_CONFIG | spi.SPI_INPUT | spi.SPI_END, 24, SPIT_RD, SPI_CS ) self.bus.write((addr << 25)) return self.bus.read() & int32(0xFFFF)
[docs] @kernel def write_reg(self, addr, data): """Write a register""" self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, 24, SPIT_WR, SPI_CS) self.bus.write((addr << 25) | WE | ((data & 0xFFFF) << 8))
[docs] @kernel def init(self, blind=False): """ Initialize and detect Mirny. Select the clock source based the board's hardware revision. Raise ValueError if the board's hardware revision is not supported. :param blind: Verify presence and protocol compatibility. Raise ValueError on failure. """ reg0 = self.read_reg(0) self.hw_rev = reg0 & 0x3 if not blind: if (reg0 >> 2) & 0x3 != PROTO_REV_MATCH: raise ValueError("Mirny PROTO_REV mismatch") delay(100 * us) # slack # select clock source self.clk_sel = self.clk_sel_hw_rev[self.hw_rev] if self.clk_sel < 0: raise ValueError("Hardware revision not supported") self.write_reg(1, (self.clk_sel << 4)) delay(1000 * us)
[docs] @portable(flags={"fast-math"}) def att_to_mu(self, att): """Convert an attenuation setting in dB to machine units. :param att: Attenuation setting in dB. :return: Digital attenuation setting. """ code = int32(255) - int32(round(att * 8)) if code < 0 or code > 255: raise ValueError("Invalid Mirny attenuation!") return code
[docs] @kernel def set_att_mu(self, channel, att): """Set digital step attenuator in machine units. :param att: Attenuation setting, 8 bit digital. """ self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, 16, SPIT_WR, SPI_CS) self.bus.write(((channel | 8) << 25) | (att << 16))
[docs] @kernel def set_att(self, channel, att): """Set digital step attenuator in SI units. This method will write the attenuator settings of the selected channel. .. seealso:: :meth:`set_att_mu` :param channel: Attenuator channel (0-3). :param att: Attenuation setting in dB. Higher value is more attenuation. Minimum attenuation is 0*dB, maximum attenuation is 31.5*dB. """ self.set_att_mu(channel, self.att_to_mu(att))
[docs] @kernel def write_ext(self, addr, length, data, ext_div=SPIT_WR): """Perform SPI write to a prefixed address""" self.bus.set_config_mu(SPI_CONFIG, 8, SPIT_WR, SPI_CS) self.bus.write(addr << 25) self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, length, ext_div, SPI_CS) if length < 32: data <<= 32 - length self.bus.write(data)
[docs]class Almazny: """ Almazny (High frequency mezzanine board for Mirny) :param host_mirny: Mirny device Almazny is connected to """ def __init__(self, dmgr, host_mirny): self.mirny_cpld = dmgr.get(host_mirny) self.att_mu = [0x3f] * 4 self.channel_sw = [0] * 4 self.output_enable = False @kernel def init(self): self.output_toggle(self.output_enable)
[docs] @kernel def att_to_mu(self, att): """ Convert an attenuator setting in dB to machine units. :param att: attenuator setting in dB [0-31.5] :return: attenuator setting in machine units """ mu = round(att * 2.0) if mu > 63 or mu < 0: raise ValueError("Invalid Almazny attenuator settings!") return mu
[docs] @kernel def mu_to_att(self, att_mu): """ Convert a digital attenuator setting to dB. :param att_mu: attenuator setting in machine units :return: attenuator setting in dB """ return att_mu / 2
[docs] @kernel def set_att(self, channel, att, rf_switch=True): """ Sets attenuators on chosen shift register (channel). :param channel: index of the register [0-3] :param att: attenuation setting in dBm [0-31.5] :param rf_switch: rf switch (bool) """ self.set_att_mu(channel, self.att_to_mu(att), rf_switch)
[docs] @kernel def set_att_mu(self, channel, att_mu, rf_switch=True): """ Sets attenuators on chosen shift register (channel). :param channel: index of the register [0-3] :param att_mu: attenuation setting in machine units [0-63] :param rf_switch: rf switch (bool) """ self.channel_sw[channel] = 1 if rf_switch else 0 self.att_mu[channel] = att_mu self._update_register(channel)
[docs] @kernel def output_toggle(self, oe): """ Toggles output on all shift registers on or off. :param oe: toggle output enable (bool) """ self.output_enable = oe cfg_reg = self.mirny_cpld.read_reg(1) en = 1 if self.output_enable else 0 delay(100 * us) new_reg = (en << ALMAZNY_OE_SHIFT) | (cfg_reg & 0x3FF) self.mirny_cpld.write_reg(1, new_reg) delay(100 * us)
@kernel def _flip_mu_bits(self, mu): # in this form MSB is actually 0.5dB attenuator # unnatural for users, so we flip the six bits return (((mu & 0x01) << 5) | ((mu & 0x02) << 3) | ((mu & 0x04) << 1) | ((mu & 0x08) >> 1) | ((mu & 0x10) >> 3) | ((mu & 0x20) >> 5)) @kernel def _update_register(self, ch): self.mirny_cpld.write_ext( ALMAZNY_REG_BASE + ch, 8, self._flip_mu_bits(self.att_mu[ch]) | (self.channel_sw[ch] << 6), ALMAZNY_SPIT_WR ) delay(100 * us) @kernel def _update_all_registers(self): for i in range(4): self._update_register(i)