mirror of https://github.com/m-labs/artiq.git
282 lines
8.5 KiB
Python
282 lines
8.5 KiB
Python
"""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
|
|
|
|
|
|
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
|
|
|
|
@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)
|
|
|
|
@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))
|
|
|
|
@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)
|
|
|
|
@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
|
|
|
|
@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))
|
|
|
|
@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))
|
|
|
|
@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)
|
|
|
|
|
|
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)
|
|
|
|
@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
|
|
|
|
@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
|
|
|
|
@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_mu - 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)
|
|
|
|
@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)
|
|
|
|
@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) |