phaser: document, elaborate comments, some fixes

This commit is contained in:
Robert Jördens 2020-09-12 17:19:10 +00:00
parent e505dfed5b
commit c3728678d6
1 changed files with 99 additions and 60 deletions

View File

@ -1,6 +1,6 @@
from artiq.language.core import kernel, delay_mu, delay from artiq.language.core import kernel, delay_mu, delay
from artiq.coredevice.rtio import rtio_output, rtio_input_data from artiq.coredevice.rtio import rtio_output, rtio_input_data
from artiq.language.units import us, ns from artiq.language.units import us, ns, MHz
from artiq.language.types import TInt32 from artiq.language.types import TInt32
@ -57,36 +57,56 @@ class Phaser:
Phaser contains a 4 channel, 1 GS/s DAC chip with integrated upconversion, Phaser contains a 4 channel, 1 GS/s DAC chip with integrated upconversion,
quadrature modulation compensation and interpolation features. quadrature modulation compensation and interpolation features.
The coredevice produces 2 IQ data streams with 25 MS/s 14 bit. Each The coredevice produces 2 IQ data streams with 25 MS/s and 14 bit per
data stream supports 5 independent numerically controlled oscillators (NCOs) quadrature. Each data stream supports 5 independent numerically controlled
added together for each channel. Together with a data clock, framing IQ oscillators (NCOs, DDSs with 32 bit frequency, 16 bit phase, 15 bit
marker, a checksum and metadata for register access the data is sent in amplitude, and phase accumulator clear functionality) added together.
groups of 8 samples over 1.5 Gb/s FastLink via a single EEM connector. See :class:`PhaserChannel` and :class:`PhaserOscillator`.
On Phaser the data streams are buffered and interpolated from 25 MS/s to 500 Together with a data clock, framing marker, a checksum and metadata for
MS/s 16 bit followed by a 500 MS/s digital upconverter in the FPGA. register access the streams are sent in groups of 8 samples over 1.5 Gb/s
FastLink via a single EEM connector from coredevice to Phaser.
On Phaser in the FPGA 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
with adjustable frequency and phase. The interpolation passband is 20 MHz
wide, passband ripple is less than 1e-3 amplitude, stopband attenuation
is better than 75 dB at offsets > 15 MHz and better than 90 dB at offsets
> 30 MHz.
The four 16 bit 500 MS/s DAC data streams are sent via a 32 bit parallel 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. LVDS bus operating at 1 Gb/s per pin pair and processed in the DAC. On the
DAC 2x interpolation, sinx/x compensation, quadrature modulator compensation,
fine and coarse mixing as well as group delay capabilities are available.
The four analog DAC outputs are passed through anti-aliasing filters and In The latency/group delay from the RTIO events setting
the baseband variant, the even channels feed 31.5 dB range and are :class:`PhaserOscillator` or :class:`PhaserChannel` DUC parameters all they
available on the front panel. The odd outputs are available on MMCX way to the DAC outputs is deterministic. This enables deterministic
connectors on board. absolute phase with respect to other RTIO input and output events.
The four analog DAC outputs are passed through anti-aliasing filters.
In the baseband variant, the even/in-phase DAC channels feed 31.5 dB range
attenuators and are available on the front panel. The odd outputs are
available at MMCX connectors on board.
In the upconverter variant, each of the two IQ (in-phase and quadrature) In the upconverter variant, each of the two IQ (in-phase and quadrature)
output pairs feeds a one quadrature upconverter with integrated PLL/VCO. output pairs feeds a one quadrature upconverter with integrated PLL/VCO.
The output from the upconverter passes through the step attenuator and is This analog quadrature upconverter supports offset tuning for carrier and
available at the front panel. sideband suppression. The output from the upconverter passes through the
31.5 dB range step attenuator and is available at the front panel.
The DAC, the TRF upconverters and the two attenuators are configured The DAC, the analog quadrature upconverters and the two attenuators are
through a shared SPI bus that is accessed and controlled via FPGA configured through a shared SPI bus that is accessed and controlled via FPGA
registers. registers.
:param channel: Base RTIO channel number :param channel: Base RTIO channel number
:param core_device: Core device name (default: "core") :param core_device: Core device name (default: "core")
:param miso_delay: Fastlink MISO signal delay to account for cable :param miso_delay: Fastlink MISO signal delay to account for cable
and buffer round trip. This might be automated later. and buffer round trip. This might be automated later.
:attr:`channel`: List of two :class:`PhaserChannel` to access oscillators
and digital upconverter.
""" """
kernel_invariants = {"core", "channel_base", "t_frame", "miso_delay"} kernel_invariants = {"core", "channel_base", "t_frame", "miso_delay"}
@ -176,7 +196,7 @@ class Phaser:
:param duty: Duty cycle (0. to 1.) :param duty: Duty cycle (0. to 1.)
""" """
pwm = int32(round(duty*255.)) pwm = int32(round(duty*255.))
if pwm < 0 or pwm > 0xff: if pwm < 0 or pwm > 255:
raise ValueError("invalid duty cycle") raise ValueError("invalid duty cycle")
self.set_fan_mu(pwm) self.set_fan_mu(pwm)
@ -191,8 +211,8 @@ class Phaser:
:param dac_resetb: Active low DAC reset pin :param dac_resetb: Active low DAC reset pin
:param dac_sleep: DAC sleep pin :param dac_sleep: DAC sleep pin
:param dac_txena: Enable DAC transmission pin :param dac_txena: Enable DAC transmission pin
:param trf0_ps: TRF0 upconverter power save :param trf0_ps: Quadrature upconverter 0 power save
:param trf1_ps: TRF1 upconverter power save :param trf1_ps: Quadrature upconverter 1 power save
:param att0_rstn: Active low attenuator 0 reset :param att0_rstn: Active low attenuator 0 reset
:param att1_rstn: Active low attenuator 1 reset :param att1_rstn: Active low attenuator 1 reset
""" """
@ -209,8 +229,8 @@ class Phaser:
Bit flags are: Bit flags are:
* `PHASER_STA_DAC_ALARM`: DAC alarm pin * `PHASER_STA_DAC_ALARM`: DAC alarm pin
* `PHASER_STA_TRF0_LD`: TRF0 lock detect pin * `PHASER_STA_TRF0_LD`: Quadrature upconverter 0 lock detect
* `PHASER_STA_TRF1_LD`: TRF1 lock detect pin * `PHASER_STA_TRF1_LD`: Quadrature upconverter 1 lock detect
* `PHASER_STA_TERM0`: ADC channel 0 termination indicator * `PHASER_STA_TERM0`: ADC channel 0 termination indicator
* `PHASER_STA_TERM1`: ADC channel 1 termination indicator * `PHASER_STA_TERM1`: ADC channel 1 termination indicator
* `PHASER_STA_SPI_IDLE`: SPI machine is idle and data registers can be * `PHASER_STA_SPI_IDLE`: SPI machine is idle and data registers can be
@ -255,7 +275,7 @@ class Phaser:
""" """
if div < 2 or div > 257: if div < 2 or div > 257:
raise ValueError("invalid divider") raise ValueError("invalid divider")
if length < 0 or length > 8: if length < 1 or length > 8:
raise ValueError("invalid length") raise ValueError("invalid length")
self.write8(PHASER_ADDR_SPI_SEL, select) self.write8(PHASER_ADDR_SPI_SEL, select)
self.write8(PHASER_ADDR_SPI_DIVLEN, (div - 2 >> 3) | (length - 1 << 5)) self.write8(PHASER_ADDR_SPI_DIVLEN, (div - 2 >> 3) | (length - 1 << 5))
@ -298,8 +318,8 @@ class Phaser:
"""Read from a DAC register. """Read from a DAC register.
:param addr: Register address to read from :param addr: Register address to read from
:param div: SPI clock divider. Needs to be at least 250 to read the :param div: SPI clock divider. Needs to be at least 250 (1 µs SPI
temperature register. clock) to read the temperature register.
""" """
t_xfer = self.core.seconds_to_mu((8 + 1)*div*4*ns) t_xfer = self.core.seconds_to_mu((8 + 1)*div*4*ns)
self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=0) self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=0)
@ -308,7 +328,7 @@ class Phaser:
self.spi_write(0) self.spi_write(0)
delay_mu(t_xfer) delay_mu(t_xfer)
data = self.spi_read() << 8 data = self.spi_read() << 8
delay(10*us) # slack delay(20*us) # slack
self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=1) self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=1)
self.spi_write(0) self.spi_write(0)
delay_mu(t_xfer) delay_mu(t_xfer)
@ -317,12 +337,27 @@ class Phaser:
class PhaserChannel: class PhaserChannel:
"""Phaser channel IQ pair""" """Phaser channel IQ pair.
kernel_invariants = {"channel", "phaser"}
def __init__(self, phaser, channel): :attr:`oscillator`: List of five :class:`PhaserOscillator`.
.. note:: The amplitude sum of the oscillators must be less than one to
avoid clipping or overflow. If any of the DDS or DUC frequencies are
non-zero, it is not sufficient to ensure that the sum in each
quadrature is within range.
.. note:: The interpolation filter on Phaser has an intrinsic sinc-like
overshoot in its step response. That overshoot is an direct consequence
of its near-brick-wall frequency response. For large and wide-band
changes in oscillator parameters, the overshoot can lead to clipping
or overflow after the interpolation. Either band-limit any changes
in the oscillator parameters or back off the amplitude sufficiently.
"""
kernel_invariants = {"index", "phaser"}
def __init__(self, phaser, index):
self.phaser = phaser self.phaser = phaser
self.channel = channel self.index = index
self.oscillator = [PhaserOscillator(self, osc) for osc in range(5)] self.oscillator = [PhaserOscillator(self, osc) for osc in range(5)]
@kernel @kernel
@ -334,7 +369,7 @@ class PhaserChannel:
:return: DAC data as 32 bit IQ. I in the 16 LSB, Q in the 16 MSB :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)) return self.phaser.read32(PHASER_ADDR_DAC0_DATA + (self.index << 4))
@kernel @kernel
def set_dac_test(self, data: TInt32): def set_dac_test(self, data: TInt32):
@ -342,20 +377,18 @@ class PhaserChannel:
:param data: 32 bit IQ test data, I in the 16 LSB, Q in the 16 MSB :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) self.phaser.write32(PHASER_ADDR_DAC0_TEST + (self.index << 4), data)
@kernel @kernel
def set_duc_cfg(self, clr=0, clr_once=0, select=0): def set_duc_cfg(self, clr=0, clr_once=0, select=0):
"""Set the digital upconverter and interpolator configuration. """Set the digital upconverter and interpolator configuration.
:param clr: Keep the phase accumulator cleared :param clr: Keep the phase accumulator cleared (persistent)
:param clr_once: Clear the phase accumulator for one cycle :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 :param select: Select the data to send to the DAC (0: DUC data, 1: test
data) data, other values: reserved)
""" """
if select < 0 or select > 3: self.phaser.write8(PHASER_ADDR_DUC0_CFG + (self.index << 4),
raise ValueError("invalid data select")
self.phaser.write8(PHASER_ADDR_DUC0_CFG + (self.channel << 4),
((clr & 1) << 0) | ((clr_once & 1) << 1) | ((clr & 1) << 0) | ((clr_once & 1) << 1) |
((select & 3) << 2)) ((select & 3) << 2))
@ -363,26 +396,27 @@ class PhaserChannel:
def set_duc_frequency_mu(self, ftw): def set_duc_frequency_mu(self, ftw):
"""Set the DUC frequency. """Set the DUC frequency.
:param ftw: DUC frequency tuning word :param ftw: DUC frequency tuning word (32 bit)
""" """
self.phaser.write32(PHASER_ADDR_DUC0_F + (self.channel << 4), ftw) self.phaser.write32(PHASER_ADDR_DUC0_F + (self.index << 4), ftw)
@kernel @kernel
def set_duc_frequency(self, frequency): def set_duc_frequency(self, frequency):
"""Set the DUC frequency. """Set the DUC frequency.
:param frequency: DUC frequency in Hz :param frequency: DUC frequency in Hz (passband from -200 MHz to
200 MHz, wrapping around at +- 250 MHz)
""" """
ftw = int32(round(frequency*((1 << 32)/500e6))) ftw = int32(round(frequency*((1 << 32)/(500*MHz))))
self.set_duc_frequency_mu(ftw) self.set_duc_frequency_mu(ftw)
@kernel @kernel
def set_duc_phase_mu(self, pow): def set_duc_phase_mu(self, pow):
"""Set the DUC phase offset """Set the DUC phase offset
:param pow: DUC phase offset word :param pow: DUC phase offset word (16 bit)
""" """
addr = PHASER_ADDR_DUC0_P + (self.channel << 4) addr = PHASER_ADDR_DUC0_P + (self.index << 4)
self.phaser.write8(addr, pow >> 8) self.phaser.write8(addr, pow >> 8)
self.phaser.write8(addr + 1, pow) self.phaser.write8(addr + 1, pow)
@ -403,7 +437,7 @@ class PhaserChannel:
""" """
div = 34 # 30 ns min period div = 34 # 30 ns min period
t_xfer = self.phaser.core.seconds_to_mu((8 + 1)*div*4*ns) 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, self.phaser.spi_cfg(select=PHASER_SEL_ATT0 << self.index, div=div,
end=1) end=1)
self.phaser.spi_write(data) self.phaser.spi_write(data)
delay_mu(t_xfer) delay_mu(t_xfer)
@ -425,17 +459,17 @@ class PhaserChannel:
The current attenuation value is read without side effects. The current attenuation value is read without side effects.
:return: Current attenuation :return: Current attenuation in machine units
""" """
div = 34 div = 34
t_xfer = self.phaser.core.seconds_to_mu((8 + 1)*div*4*ns) 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, self.phaser.spi_cfg(select=PHASER_SEL_ATT0 << self.index, div=div,
end=0) end=0)
self.phaser.spi_write(0) self.phaser.spi_write(0)
delay_mu(t_xfer) delay_mu(t_xfer)
data = self.phaser.spi_read() data = self.phaser.spi_read()
delay(10*us) # slack delay(20*us) # slack
self.phaser.spi_cfg(select=PHASER_SEL_ATT0 << self.channel, div=div, self.phaser.spi_cfg(select=PHASER_SEL_ATT0 << self.index, div=div,
end=1) end=1)
self.phaser.spi_write(data) self.phaser.spi_write(data)
delay_mu(t_xfer) delay_mu(t_xfer)
@ -443,7 +477,7 @@ class PhaserChannel:
@kernel @kernel
def trf_write(self, data, readback=False): def trf_write(self, data, readback=False):
"""Write 32 bits to a TRF upconverter. """Write 32 bits to upconverter.
:param data: Register data (32 bit) containing encoded address :param data: Register data (32 bit) containing encoded address
:param readback: Whether to return the read back MISO data :param readback: Whether to return the read back MISO data
@ -459,7 +493,7 @@ class PhaserChannel:
if i == 0 or i == 3: if i == 0 or i == 3:
if i == 3: if i == 3:
end = 1 end = 1
self.phaser.spi_cfg(select=PHASER_SEL_TRF0 << self.channel, self.phaser.spi_cfg(select=PHASER_SEL_TRF0 << self.index,
div=div, lsb_first=1, clk_phase=clk_phase, div=div, lsb_first=1, clk_phase=clk_phase,
end=end) end=end)
self.phaser.spi_write(data) self.phaser.spi_write(data)
@ -468,34 +502,38 @@ class PhaserChannel:
if readback: if readback:
read >>= 8 read >>= 8
read |= self.phaser.spi_read() << 24 read |= self.phaser.spi_read() << 24
delay(10*us) # slack delay(20*us) # slack
return read return read
@kernel @kernel
def trf_read(self, addr, cnt_mux_sel=0) -> TInt32: def trf_read(self, addr, cnt_mux_sel=0) -> TInt32:
"""TRF upconverter register read. """Quadrature upconverter register read.
:param addr: Register address to read (0 to 7) :param addr: Register address to read (0 to 7)
:param cnt_mux_sel: Report VCO counter min frequency :param cnt_mux_sel: Report VCO counter min or max frequency
or max frequency
:return: Register data (32 bit) :return: Register data (32 bit)
""" """
self.trf_write(0x80000008 | (addr << 28) | (cnt_mux_sel << 27)) self.trf_write(0x80000008 | (addr << 28) | (cnt_mux_sel << 27))
# single clk pulse with ~LE to start readback # single clk pulse with ~LE to start readback
self.phaser.spi_cfg(select=0, div=34, end=1, length=1) self.phaser.spi_cfg(select=0, div=34, end=1, length=1)
self.phaser.spi_write(0) self.phaser.spi_write(0)
delay((1 + 1)*32*4*ns) delay((1 + 1)*34*4*ns)
return self.trf_write(0x00000008, readback=True) return self.trf_write(0x00000008, readback=True)
class PhaserOscillator: class PhaserOscillator:
"""Phaser IQ channel oscillator""" """Phaser IQ channel oscillator (NCO/DDS).
.. note:: Latencies between oscillators within a channel and between
oscillator paramters (amplitude and phase/frequency) are deterministic
(with respect to the 25 MS/s sample clock) but not matched.
"""
kernel_invariants = {"channel", "base_addr"} kernel_invariants = {"channel", "base_addr"}
def __init__(self, channel, oscillator): def __init__(self, channel, index):
self.channel = channel self.channel = channel
self.base_addr = ((self.channel.phaser.channel_base + 1 + self.base_addr = ((self.channel.phaser.channel_base + 1 +
self.channel.channel) << 8) | (oscillator << 1) self.channel.index) << 8) | (index << 1)
@kernel @kernel
def set_frequency_mu(self, ftw): def set_frequency_mu(self, ftw):
@ -509,9 +547,10 @@ class PhaserOscillator:
def set_frequency(self, frequency): def set_frequency(self, frequency):
"""Set Phaser MultiDDS frequency. """Set Phaser MultiDDS frequency.
:param frequency: Frequency in Hz :param frequency: Frequency in Hz (passband from -10 MHz to 10 MHz,
wrapping around at +- 12.5 MHz)
""" """
ftw = int32(round(frequency*((1 << 32)/125e6))) ftw = int32(round(frequency*((1 << 32)/(25*MHz))))
self.set_frequency_mu(ftw) self.set_frequency_mu(ftw)
@kernel @kernel