artiq/artiq/coredevice/phaser.py

541 lines
18 KiB
Python
Raw Normal View History

2020-08-26 23:10:50 +08:00
from artiq.language.core import kernel, delay_mu, delay
2020-08-24 23:46:31 +08:00
from artiq.coredevice.rtio import rtio_output, rtio_input_data
2020-08-26 23:10:50 +08:00
from artiq.language.units import us, ns
from artiq.language.types import TInt32
2020-08-24 23:46:31 +08:00
PHASER_BOARD_ID = 19
2020-08-26 23:10:50 +08:00
PHASER_ADDR_BOARD_ID = 0x00
PHASER_ADDR_HW_REV = 0x01
PHASER_ADDR_GW_REV = 0x02
PHASER_ADDR_CFG = 0x03
PHASER_ADDR_STA = 0x04
PHASER_ADDR_CRC_ERR = 0x05
PHASER_ADDR_LED = 0x06
PHASER_ADDR_FAN = 0x07
PHASER_ADDR_DUC_STB = 0x08
PHASER_ADDR_ADC_CFG = 0x09
PHASER_ADDR_SPI_CFG = 0x0a
PHASER_ADDR_SPI_DIVLEN = 0x0b
2020-08-26 23:10:50 +08:00
PHASER_ADDR_SPI_SEL = 0x0c
PHASER_ADDR_SPI_DATW = 0x0d
PHASER_ADDR_SPI_DATR = 0x0e
# PHASER_ADDR_RESERVED0 = 0x0f
PHASER_ADDR_DUC0_CFG = 0x10
# PHASER_ADDR_DUC0_RESERVED0 = 0x11
PHASER_ADDR_DUC0_F = 0x12
PHASER_ADDR_DUC0_P = 0x16
PHASER_ADDR_DAC0_DATA = 0x18
PHASER_ADDR_DAC0_TEST = 0x1c
PHASER_ADDR_DUC1_CFG = 0x20
# PHASER_ADDR_DUC1_RESERVED0 = 0x21
PHASER_ADDR_DUC1_F = 0x22
PHASER_ADDR_DUC1_P = 0x26
PHASER_ADDR_DAC1_DATA = 0x28
PHASER_ADDR_DAC1_TEST = 0x2c
PHASER_SEL_DAC = 1 << 0
PHASER_SEL_TRF0 = 1 << 1
PHASER_SEL_TRF1 = 1 << 2
PHASER_SEL_ATT0 = 1 << 3
PHASER_SEL_ATT1 = 1 << 4
PHASER_STA_DAC_ALARM = 1 << 0
PHASER_STA_TRF0_LD = 1 << 1
PHASER_STA_TRF1_LD = 1 << 2
PHASER_STA_TERM0 = 1 << 3
PHASER_STA_TERM1 = 1 << 4
PHASER_STA_SPI_IDLE = 1 << 5
2020-08-29 00:36:44 +08:00
PHASER_DAC_SEL_DUC = 0
PHASER_DAC_SEL_TEST = 1
2020-08-24 23:46:31 +08:00
class Phaser:
2020-08-29 00:36:44 +08:00
"""Phaser 4-channel, 16-bit, 1 GS/s DAC coredevice driver.
Phaser contains a 4 channel, 1 GS/s DAC chip with integrated upconversion,
quadrature modulation compensation and interpolation features.
The coredevice produces 2 IQ data streams with 25 MS/s 14 bit. Each
data stream supports 5 independent numerically controlled oscillators (NCOs)
added together for each channel. Together with a data clock, framing
marker, a checksum and metadata for register access the data is sent in
groups of 8 samples over 1.5 Gb/s FastLink via a single EEM connector.
On Phaser the data streams are buffered and interpolated from 25 MS/s to 500
MS/s 16 bit followed by a 500 MS/s digital upconverter in the FPGA.
The four 16 bit 500 MS/s DAC data streams are sent via a 32 bit parallel
LVDS bus operating at 1 Gb/s per pin pair and processed in the DAC.
The four analog DAC outputs are passed through anti-aliasing filters and In
the baseband variant, the even channels feed 31.5 dB range and are
available on the front panel. The odd outputs are available on MMCX
connectors on board.
2020-08-24 23:46:31 +08:00
2020-08-29 00:36:44 +08:00
In the upconverter variant, each of the two IQ (in-phase and quadrature)
output pairs feeds a one quadrature upconverter with integrated PLL/VCO.
The output from the upconverter passes through the step attenuator and is
available at the front panel.
The DAC, the TRF upconverters and the two attenuators are configured
through a shared SPI bus that is accessed and controlled via FPGA
registers.
:param channel: Base RTIO channel number
:param core_device: Core device name (default: "core")
:param miso_delay: Fastlink MISO signal delay to account for cable
and buffer round trip. This might be automated later.
"""
kernel_invariants = {"core", "channel_base", "t_frame", "miso_delay"}
def __init__(self, dmgr, channel_base, miso_delay=1, core_device="core"):
2020-08-27 01:12:41 +08:00
self.channel_base = channel_base
2020-08-24 23:46:31 +08:00
self.core = dmgr.get(core_device)
2020-09-12 22:17:40 +08:00
# TODO: auto-align miso-delay in phy
2020-08-26 23:10:50 +08:00
self.miso_delay = miso_delay
# frame duration in mu (10 words, 8 clock cycles each 4 ns)
2020-08-29 00:36:44 +08:00
# self.core.seconds_to_mu(10*8*4*ns) # unfortunately this returns 319
assert self.core.ref_period == 1*ns
2020-08-26 23:10:50 +08:00
self.t_frame = 10*8*4
2020-08-24 23:46:31 +08:00
2020-09-12 22:17:40 +08:00
self.channel = [PhaserChannel(self, ch) for ch in range(2)]
2020-08-24 23:46:31 +08:00
@kernel
def init(self):
2020-08-29 00:36:44 +08:00
"""Initialize the board.
Verifies board presence by reading the board ID register.
Does not alter any state.
"""
board_id = self.read8(PHASER_ADDR_BOARD_ID)
2020-08-24 23:46:31 +08:00
if board_id != PHASER_BOARD_ID:
raise ValueError("invalid board id")
2020-08-29 00:36:44 +08:00
delay(20*us) # slack
2020-08-24 23:46:31 +08:00
@kernel
def write8(self, addr, data):
2020-08-29 00:36:44 +08:00
"""Write data to FPGA register.
2020-08-24 23:46:31 +08:00
2020-08-29 00:36:44 +08:00
:param addr: Address to write to (7 bit)
:param data: Data to write (8 bit)
2020-08-24 23:46:31 +08:00
"""
rtio_output((self.channel_base << 8) | (addr & 0x7f) | 0x80, data)
2020-08-26 23:10:50 +08:00
delay_mu(int64(self.t_frame))
2020-08-24 23:46:31 +08:00
@kernel
def read8(self, addr) -> TInt32:
2020-08-29 00:36:44 +08:00
"""Read from FPGA register.
2020-08-24 23:46:31 +08:00
2020-08-29 00:36:44 +08:00
:param addr: Address to read from (7 bit)
:return: Data read (8 bit)
2020-08-24 23:46:31 +08:00
"""
rtio_output((self.channel_base << 8) | (addr & 0x7f), 0)
2020-08-27 01:12:41 +08:00
response = rtio_input_data(self.channel_base)
2020-08-26 23:10:50 +08:00
return response >> self.miso_delay
@kernel
def write32(self, addr, data: TInt32):
2020-08-29 00:36:44 +08:00
"""Write 32 bit to a sequence of FPGA registers."""
for offset in range(4):
byte = data >> 24
self.write8(addr + offset, byte)
data <<= 8
@kernel
def read32(self, addr) -> TInt32:
2020-08-29 00:36:44 +08:00
"""Read 32 bit from a sequence of FPGA registers."""
data = 0
for offset in range(4):
data <<= 8
data |= self.read8(addr + offset)
delay(20*us) # slack
return data
2020-08-26 23:10:50 +08:00
@kernel
def set_leds(self, leds):
2020-08-29 00:36:44 +08:00
"""Set the front panel LEDs.
:param leds: LED settings (6 bit)
"""
self.write8(PHASER_ADDR_LED, leds)
2020-08-26 23:10:50 +08:00
2020-09-12 22:17:40 +08:00
@kernel
def set_fan_mu(self, pwm):
"""Set the fan duty cycle.
:param pwm: Duty cycle (8 bit)
"""
self.write8(PHASER_ADDR_FAN, pwm)
2020-08-26 23:10:50 +08:00
@kernel
def set_fan(self, duty):
2020-08-29 00:36:44 +08:00
"""Set the fan duty cycle.
2020-09-12 22:17:40 +08:00
:param duty: Duty cycle (0. to 1.)
2020-08-29 00:36:44 +08:00
"""
2020-09-12 22:17:40 +08:00
pwm = int32(round(duty*255.))
if pwm < 0 or pwm > 0xff:
raise ValueError("invalid duty cycle")
self.set_fan_mu(pwm)
2020-08-26 23:10:50 +08:00
@kernel
def set_cfg(self, clk_sel=0, dac_resetb=1, dac_sleep=0, dac_txena=1,
trf0_ps=0, trf1_ps=0, att0_rstn=1, att1_rstn=1):
2020-08-29 00:36:44 +08:00
"""Set the configuration register.
2020-09-12 22:17:40 +08:00
Each flag is a single bit (0 or 1).
2020-08-29 00:36:44 +08:00
:param clk_sel: Select the external SMA clock input
:param dac_resetb: Active low DAC reset pin
:param dac_sleep: DAC sleep pin
:param dac_txena: Enable DAC transmission pin
:param trf0_ps: TRF0 upconverter power save
:param trf1_ps: TRF1 upconverter power save
:param att0_rstn: Active low attenuator 0 reset
:param att1_rstn: Active low attenuator 1 reset
"""
self.write8(PHASER_ADDR_CFG,
2020-09-12 22:17:40 +08:00
((clk_sel & 1) << 0) | ((dac_resetb & 1) << 1) |
((dac_sleep & 1) << 2) | ((dac_txena & 1) << 3) |
((trf0_ps & 1) << 4) | ((trf1_ps & 1) << 5) |
((att0_rstn & 1) << 6) | ((att1_rstn & 1) << 7))
2020-08-26 23:10:50 +08:00
@kernel
def get_sta(self):
2020-08-29 00:36:44 +08:00
"""Get the status register value.
Bit flags are:
* `PHASER_STA_DAC_ALARM`: DAC alarm pin
* `PHASER_STA_TRF0_LD`: TRF0 lock detect pin
* `PHASER_STA_TRF1_LD`: TRF1 lock detect pin
* `PHASER_STA_TERM0`: ADC channel 0 termination indicator
* `PHASER_STA_TERM1`: ADC channel 1 termination indicator
* `PHASER_STA_SPI_IDLE`: SPI machine is idle and data registers can be
read/written
:return: Status register
"""
return self.read8(PHASER_ADDR_STA)
2020-08-26 23:10:50 +08:00
@kernel
def get_crc_err(self):
2020-09-12 22:17:40 +08:00
"""Get the frame CRC error counter.
2020-08-29 00:36:44 +08:00
2020-09-12 22:17:40 +08:00
:return: The number of frames with CRC mismatches sind the reset of the
device. Overflows at 256.
2020-08-29 00:36:44 +08:00
"""
2020-09-12 22:17:40 +08:00
return self.read8(PHASER_ADDR_CRC_ERR)
2020-09-12 19:02:37 +08:00
@kernel
def duc_stb(self):
2020-08-29 00:36:44 +08:00
"""Strobe the DUC configuration register update.
Transfer staging to active registers.
This affects both DUC channels.
"""
self.write8(PHASER_ADDR_DUC_STB, 0)
2020-08-26 23:10:50 +08:00
@kernel
def spi_cfg(self, select, div, end, clk_phase=0, clk_polarity=0,
half_duplex=0, lsb_first=0, offline=0, length=8):
2020-08-29 00:36:44 +08:00
"""Set the SPI machine configuration
:param select: Chip selects to assert (DAC, TRF0, TRF1, ATT0, ATT1)
:param div: SPI clock divider relative to 250 MHz fabric clock
:param end: Whether to end the SPI transaction and deassert chip select
:param clk_phase: SPI clock phase (sample on first or second edge)
:param clk_polarity: SPI clock polarity (idle low or high)
:param half_duplex: Read MISO data from MOSI wire
:param lsb_first: Transfer the least significant bit first
:param offline: Put the SPI interfaces offline and don't drive voltages
:param length: SPI transfer length (1 to 8 bits)
"""
2020-09-12 19:02:37 +08:00
if div < 2 or div > 257:
raise ValueError("invalid divider")
if length < 0 or length > 8:
raise ValueError("invalid length")
self.write8(PHASER_ADDR_SPI_SEL, select)
self.write8(PHASER_ADDR_SPI_DIVLEN, (div - 2 >> 3) | (length - 1 << 5))
self.write8(PHASER_ADDR_SPI_CFG,
2020-09-12 19:02:37 +08:00
((offline & 1) << 0) | ((end & 1) << 1) |
((clk_phase & 1) << 2) | ((clk_polarity & 1) << 3) |
((half_duplex & 1) << 4) | ((lsb_first & 1) << 5))
2020-08-26 23:10:50 +08:00
@kernel
def spi_write(self, data):
2020-08-29 00:36:44 +08:00
"""Write 8 bits into the SPI data register and start/continue the
transaction."""
self.write8(PHASER_ADDR_SPI_DATW, data)
2020-08-26 23:10:50 +08:00
@kernel
def spi_read(self):
2020-08-29 00:36:44 +08:00
"""Read from the SPI input data register."""
return self.read8(PHASER_ADDR_SPI_DATR)
2020-08-26 23:10:50 +08:00
@kernel
def dac_write(self, addr, data):
2020-08-29 00:36:44 +08:00
"""Write 16 bit to a DAC register.
:param addr: Register address
:param data: Register data to write
"""
2020-09-12 19:02:37 +08:00
div = 34 # 100 ns min period
t_xfer = self.core.seconds_to_mu((8 + 1)*div*4*ns)
2020-08-26 23:10:50 +08:00
self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=0)
self.spi_write(addr)
delay_mu(t_xfer)
self.spi_write(data >> 8)
delay_mu(t_xfer)
self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=1)
self.spi_write(data)
2020-08-26 23:10:50 +08:00
delay_mu(t_xfer)
@kernel
2020-09-12 19:02:37 +08:00
def dac_read(self, addr, div=34) -> TInt32:
2020-08-29 00:36:44 +08:00
"""Read from a DAC register.
:param addr: Register address to read from
:param div: SPI clock divider. Needs to be at least 250 to read the
temperature register.
"""
t_xfer = self.core.seconds_to_mu((8 + 1)*div*4*ns)
2020-08-26 23:10:50 +08:00
self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=0)
self.spi_write(addr | 0x80)
delay_mu(t_xfer)
self.spi_write(0)
delay_mu(t_xfer)
data = self.spi_read() << 8
delay(10*us) # slack
self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=1)
self.spi_write(0)
delay_mu(t_xfer)
data |= self.spi_read()
return data
2020-09-12 22:17:40 +08:00
class PhaserChannel:
"""Phaser channel IQ pair"""
kernel_invariants = {"channel", "phaser"}
def __init__(self, phaser, channel):
self.phaser = phaser
self.channel = channel
self.oscillator = [PhaserOscillator(self, osc) for osc in range(5)]
2020-08-26 23:10:50 +08:00
@kernel
2020-09-12 22:17:40 +08:00
def get_dac_data(self) -> TInt32:
"""Get a sample of the current DAC data.
The data is split accross multiple registers and thus the data
is only valid if constant.
:return: DAC data as 32 bit IQ. I in the 16 LSB, Q in the 16 MSB
"""
return self.phaser.read32(PHASER_ADDR_DAC0_DATA + (self.channel << 4))
@kernel
def set_dac_test(self, data: TInt32):
"""Set the DAC test data.
:param data: 32 bit IQ test data, I in the 16 LSB, Q in the 16 MSB
"""
self.phaser.write32(PHASER_ADDR_DAC0_TEST + (self.channel << 4), data)
@kernel
def set_duc_cfg(self, clr=0, clr_once=0, select=0):
"""Set the digital upconverter and interpolator configuration.
:param clr: Keep the phase accumulator cleared
:param clr_once: Clear the phase accumulator for one cycle
:param select: Select the data to send to the DAC (0: DUC data, 1: test
data)
"""
if select < 0 or select > 3:
raise ValueError("invalid data select")
self.phaser.write8(PHASER_ADDR_DUC0_CFG + (self.channel << 4),
((clr & 1) << 0) | ((clr_once & 1) << 1) |
((select & 3) << 2))
@kernel
def set_duc_frequency_mu(self, ftw):
"""Set the DUC frequency.
:param ftw: DUC frequency tuning word
"""
self.phaser.write32(PHASER_ADDR_DUC0_F + (self.channel << 4), ftw)
@kernel
def set_duc_frequency(self, frequency):
"""Set the DUC frequency.
:param frequency: DUC frequency in Hz
"""
ftw = int32(round(frequency*((1 << 32)/500e6)))
self.set_duc_frequency_mu(ftw)
@kernel
def set_duc_phase_mu(self, pow):
"""Set the DUC phase offset
:param pow: DUC phase offset word
"""
addr = PHASER_ADDR_DUC0_P + (self.channel << 4)
self.phaser.write8(addr, pow >> 8)
self.phaser.write8(addr + 1, pow)
@kernel
def set_duc_phase(self, phase):
"""Set the DUC phase.
:param phase: DUC phase in turns
"""
pow = int32(round(phase*(1 << 16)))
self.set_duc_phase_mu(pow)
@kernel
def set_att_mu(self, data):
2020-08-29 00:36:44 +08:00
"""Set channel attenuation.
:param data: Attenuator data
"""
2020-09-12 19:02:37 +08:00
div = 34 # 30 ns min period
2020-09-12 22:17:40 +08:00
t_xfer = self.phaser.core.seconds_to_mu((8 + 1)*div*4*ns)
self.phaser.spi_cfg(select=PHASER_SEL_ATT0 << self.channel, div=div,
end=1)
self.phaser.spi_write(data)
2020-08-26 23:10:50 +08:00
delay_mu(t_xfer)
@kernel
2020-09-12 22:17:40 +08:00
def set_att(self, att):
"""Set channel attenuation in SI units.
:param att: Attenuation in dB
"""
data = 0xff - int32(round(att*8))
if data < 0 or data > 0xff:
raise ValueError("invalid attenuation")
self.set_att_mu(data)
@kernel
def get_att_mu(self) -> TInt32:
2020-08-29 00:36:44 +08:00
"""Read current attenuation.
The current attenuation value is read without side effects.
:return: Current attenuation
"""
2020-09-12 19:02:37 +08:00
div = 34
2020-09-12 22:17:40 +08:00
t_xfer = self.phaser.core.seconds_to_mu((8 + 1)*div*4*ns)
self.phaser.spi_cfg(select=PHASER_SEL_ATT0 << self.channel, div=div,
end=0)
self.phaser.spi_write(0)
2020-08-26 23:10:50 +08:00
delay_mu(t_xfer)
2020-09-12 22:17:40 +08:00
data = self.phaser.spi_read()
delay(10*us) # slack
2020-09-12 22:17:40 +08:00
self.phaser.spi_cfg(select=PHASER_SEL_ATT0 << self.channel, div=div,
end=1)
self.phaser.spi_write(data)
2020-08-26 23:10:50 +08:00
delay_mu(t_xfer)
return data
2020-08-27 01:12:41 +08:00
@kernel
2020-09-12 22:17:40 +08:00
def trf_write(self, data, readback=False):
2020-08-29 00:36:44 +08:00
"""Write 32 bits to a TRF upconverter.
2020-09-12 22:17:40 +08:00
:param data: Register data (32 bit) containing encoded address
2020-08-29 00:36:44 +08:00
:param readback: Whether to return the read back MISO data
"""
2020-09-12 19:02:37 +08:00
div = 34 # 50 ns min period
2020-09-12 22:17:40 +08:00
t_xfer = self.phaser.core.seconds_to_mu((8 + 1)*div*4*ns)
read = 0
end = 0
clk_phase = 0
if readback:
clk_phase = 1
for i in range(4):
if i == 0 or i == 3:
if i == 3:
end = 1
2020-09-12 22:17:40 +08:00
self.phaser.spi_cfg(select=PHASER_SEL_TRF0 << self.channel,
div=div, lsb_first=1, clk_phase=clk_phase,
end=end)
self.phaser.spi_write(data)
data >>= 8
delay_mu(t_xfer)
if readback:
read >>= 8
2020-09-12 22:17:40 +08:00
read |= self.phaser.spi_read() << 24
delay(10*us) # slack
return read
@kernel
2020-09-12 22:17:40 +08:00
def trf_read(self, addr, cnt_mux_sel=0) -> TInt32:
2020-08-29 00:36:44 +08:00
"""TRF upconverter register read.
2020-09-12 22:17:40 +08:00
:param addr: Register address to read (0 to 7)
2020-08-29 00:36:44 +08:00
:param cnt_mux_sel: Report VCO counter min frequency
or max frequency
:return: Register data (32 bit)
"""
2020-09-12 22:17:40 +08:00
self.trf_write(0x80000008 | (addr << 28) | (cnt_mux_sel << 27))
2020-08-29 00:36:44 +08:00
# single clk pulse with ~LE to start readback
2020-09-12 22:17:40 +08:00
self.phaser.spi_cfg(select=0, div=34, end=1, length=1)
self.phaser.spi_write(0)
delay((1 + 1)*32*4*ns)
2020-09-12 22:17:40 +08:00
return self.trf_write(0x00000008, readback=True)
class PhaserOscillator:
"""Phaser IQ channel oscillator"""
kernel_invariants = {"channel", "base_addr"}
def __init__(self, channel, oscillator):
self.channel = channel
self.base_addr = ((self.channel.phaser.channel_base + 1 +
self.channel.channel) << 8) | (oscillator << 1)
2020-08-27 01:12:41 +08:00
@kernel
2020-09-12 22:17:40 +08:00
def set_frequency_mu(self, ftw):
2020-08-29 00:36:44 +08:00
"""Set Phaser MultiDDS frequency tuning word.
:param ftw: Frequency tuning word (32 bit)
"""
2020-09-12 22:17:40 +08:00
rtio_output(self.base_addr, ftw)
2020-08-27 01:12:41 +08:00
2020-09-12 19:02:37 +08:00
@kernel
2020-09-12 22:17:40 +08:00
def set_frequency(self, frequency):
2020-09-12 19:02:37 +08:00
"""Set Phaser MultiDDS frequency.
:param frequency: Frequency in Hz
"""
ftw = int32(round(frequency*((1 << 32)/125e6)))
2020-09-12 22:17:40 +08:00
self.set_frequency_mu(ftw)
2020-09-12 19:02:37 +08:00
2020-08-27 01:12:41 +08:00
@kernel
2020-09-12 22:17:40 +08:00
def set_amplitude_phase_mu(self, asf=0x7fff, pow=0, clr=0):
2020-08-29 00:36:44 +08:00
"""Set Phaser MultiDDS amplitude, phase offset and accumulator clear.
:param asf: Amplitude (15 bit)
:param pow: Phase offset word (16 bit)
:param clr: Clear the phase accumulator (persistent)
"""
2020-09-12 22:17:40 +08:00
data = (asf & 0x7fff) | ((clr & 1) << 15) | (pow << 16)
rtio_output(self.base_addr | 1, data)
2020-09-12 19:02:37 +08:00
@kernel
2020-09-12 22:17:40 +08:00
def set_amplitude_phase(self, amplitude, phase=0., clr=0):
2020-09-12 19:02:37 +08:00
"""Set Phaser MultiDDS amplitude and phase.
:param amplitude: Amplitude in units of full scale
:param phase: Phase in turns
:param clr: Clear the phase accumulator (persistent)
"""
asf = int32(round(amplitude*0x7fff))
if asf < 0 or asf > 0x7fff:
raise ValueError("invalid amplitude")
pow = int32(round(phase*(1 << 16)))
2020-09-12 22:17:40 +08:00
self.set_amplitude_phase_mu(asf, pow, clr)