forked from M-Labs/artiq
1
0
Fork 0

phaser: documentation

This commit is contained in:
Robert Jördens 2020-08-28 16:36:44 +00:00
parent 68bfa04abb
commit 272dc5d36a
3 changed files with 208 additions and 25 deletions

View File

@ -1,5 +1,3 @@
import numpy as np
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
@ -49,44 +47,86 @@ PHASER_STA_TERM0 = 1 << 3
PHASER_STA_TERM1 = 1 << 4 PHASER_STA_TERM1 = 1 << 4
PHASER_STA_SPI_IDLE = 1 << 5 PHASER_STA_SPI_IDLE = 1 << 5
PHASER_DAC_SEL_DUC = 0
PHASER_DAC_SEL_TEST = 1
class Phaser: class Phaser:
kernel_invariants = {"core", "channel_base", "t_frame"} """Phaser 4-channel, 16-bit, 1 GS/s DAC coredevice driver.
def __init__(self, dmgr, channel_base, miso_delay=1, Phaser contains a 4 channel, 1 GS/s DAC chip with integrated upconversion,
core_device="core"): 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.
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"):
self.channel_base = channel_base self.channel_base = channel_base
self.core = dmgr.get(core_device) self.core = dmgr.get(core_device)
self.miso_delay = miso_delay self.miso_delay = miso_delay
# frame duration in mu (10 words, 8 clock cycles each 4 ns) # frame duration in mu (10 words, 8 clock cycles each 4 ns)
# self.core.seconds_to_mu(10*8*4*ns) # unfortunately 319 # self.core.seconds_to_mu(10*8*4*ns) # unfortunately this returns 319
assert self.core.ref_period == 1*ns
self.t_frame = 10*8*4 self.t_frame = 10*8*4
@kernel @kernel
def init(self): def init(self):
"""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) board_id = self.read8(PHASER_ADDR_BOARD_ID)
if board_id != PHASER_BOARD_ID: if board_id != PHASER_BOARD_ID:
raise ValueError("invalid board id") raise ValueError("invalid board id")
delay(20*us) delay(20*us) # slack
@kernel @kernel
def write8(self, addr, data): def write8(self, addr, data):
"""Write data to a Phaser FPGA register. """Write data to FPGA register.
:param addr: Address to write to. :param addr: Address to write to (7 bit)
:param data: Data to write. :param data: Data to write (8 bit)
""" """
rtio_output((self.channel_base << 8) | (addr & 0x7f) | 0x80, data) rtio_output((self.channel_base << 8) | (addr & 0x7f) | 0x80, data)
delay_mu(int64(self.t_frame)) delay_mu(int64(self.t_frame))
@kernel @kernel
def read8(self, addr) -> TInt32: def read8(self, addr) -> TInt32:
"""Read from Phaser FPGA register. """Read from FPGA register.
TODO: untested :param addr: Address to read from (7 bit)
:return: Data read (8 bit)
:param addr: Address to read from.
:return: The data read.
""" """
rtio_output((self.channel_base << 8) | (addr & 0x7f), 0) rtio_output((self.channel_base << 8) | (addr & 0x7f), 0)
response = rtio_input_data(self.channel_base) response = rtio_input_data(self.channel_base)
@ -94,6 +134,7 @@ class Phaser:
@kernel @kernel
def write32(self, addr, data: TInt32): def write32(self, addr, data: TInt32):
"""Write 32 bit to a sequence of FPGA registers."""
for offset in range(4): for offset in range(4):
byte = data >> 24 byte = data >> 24
self.write8(addr + offset, byte) self.write8(addr + offset, byte)
@ -101,6 +142,7 @@ class Phaser:
@kernel @kernel
def read32(self, addr) -> TInt32: def read32(self, addr) -> TInt32:
"""Read 32 bit from a sequence of FPGA registers."""
data = 0 data = 0
for offset in range(4): for offset in range(4):
data <<= 8 data <<= 8
@ -110,39 +152,80 @@ class Phaser:
@kernel @kernel
def write16(self, addr, data: TInt32): def write16(self, addr, data: TInt32):
"""Write 16 bit to a sequence of FPGA registers."""
self.write8(addr, data >> 8) self.write8(addr, data >> 8)
self.write8(addr + 1, data) self.write8(addr + 1, data)
@kernel @kernel
def read16(self, addr) -> TInt32: def read16(self, addr) -> TInt32:
"""Read 16 bit from a sequence of FPGA registers."""
return (self.read8(addr) << 8) | self.read8(addr) return (self.read8(addr) << 8) | self.read8(addr)
@kernel @kernel
def set_leds(self, leds): def set_leds(self, leds):
"""Set the front panel LEDs.
:param leds: LED settings (6 bit)
"""
self.write8(PHASER_ADDR_LED, leds) self.write8(PHASER_ADDR_LED, leds)
@kernel @kernel
def set_fan(self, duty): def set_fan(self, duty):
"""Set the fan duty cycle.
:param duty: Duty cycle (8 bit)
"""
self.write8(PHASER_ADDR_FAN, duty) self.write8(PHASER_ADDR_FAN, duty)
@kernel @kernel
def set_cfg(self, clk_sel=0, dac_resetb=1, dac_sleep=0, dac_txena=1, 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): trf0_ps=0, trf1_ps=0, att0_rstn=1, att1_rstn=1):
"""Set the configuration register.
: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, self.write8(PHASER_ADDR_CFG,
(clk_sel << 0) | (dac_resetb << 1) | (dac_sleep << 2) | (clk_sel << 0) | (dac_resetb << 1) | (dac_sleep << 2) |
(dac_txena << 3) | (trf0_ps << 4) | (trf1_ps << 5) | (dac_txena << 3) | (trf0_ps << 4) | (trf1_ps << 5) |
(att0_rstn << 6) | (att1_rstn << 7)) (att0_rstn << 6) | (att1_rstn << 7))
@kernel @kernel
def get_sta(self): def get_sta(self):
"""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) return self.read8(PHASER_ADDR_STA)
@kernel @kernel
def get_crc_err(self): def get_crc_err(self):
"""Get the frame CRC error counter."""
return self.read8(PHASER_ADDR_CRC_ERR) return self.read8(PHASER_ADDR_CRC_ERR)
@kernel @kernel
def get_dac_data(self, ch) -> TInt32: def get_dac_data(self, ch) -> TInt32:
"""Get a sample of the current DAC data.
:param ch: DAC channel pair (0 or 1)
:return: DAC data as 32 bit IQ
"""
data = 0 data = 0
for addr in range(4): for addr in range(4):
data <<= 8 data <<= 8
@ -152,6 +235,11 @@ class Phaser:
@kernel @kernel
def set_dac_test(self, ch, data: TInt32): def set_dac_test(self, ch, data: TInt32):
"""Set the DAC test data.
:param ch: DAC channel pair (0 or 1)
:param data: 32 bit IQ test data
"""
for addr in range(4): for addr in range(4):
byte = data >> 24 byte = data >> 24
self.write8(PHASER_ADDR_DAC0_TEST + (ch << 4) + addr, byte) self.write8(PHASER_ADDR_DAC0_TEST + (ch << 4) + addr, byte)
@ -159,41 +247,84 @@ class Phaser:
@kernel @kernel
def set_duc_cfg(self, ch, clr=0, clr_once=0, select=0): def set_duc_cfg(self, ch, clr=0, clr_once=0, select=0):
"""Set the digital upconverter and interpolator configuration.
:param ch: DAC channel pair (0 or 1)
: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)
"""
self.write8(PHASER_ADDR_DUC0_CFG + (ch << 4), self.write8(PHASER_ADDR_DUC0_CFG + (ch << 4),
(clr << 0) | (clr_once << 1) | (select << 2)) (clr << 0) | (clr_once << 1) | (select << 2))
@kernel @kernel
def set_duc_frequency_mu(self, ch, ftw): def set_duc_frequency_mu(self, ch, ftw):
"""Set the DUC frequency.
:param ch: DAC channel pair (0 or 1)
:param ftw: DUC frequency tuning word
"""
self.write32(PHASER_ADDR_DUC0_F + (ch << 4), ftw) self.write32(PHASER_ADDR_DUC0_F + (ch << 4), ftw)
@kernel @kernel
def set_duc_phase_mu(self, ch, pow): def set_duc_phase_mu(self, ch, pow):
"""Set the DUC phase offset
:param ch: DAC channel pair (0 or 1)
:param pow: DUC phase offset word
"""
self.write16(PHASER_ADDR_DUC0_P + (ch << 4), pow) self.write16(PHASER_ADDR_DUC0_P + (ch << 4), pow)
@kernel @kernel
def duc_stb(self): def duc_stb(self):
"""Strobe the DUC configuration register update.
Transfer staging to active registers.
This affects both DUC channels.
"""
self.write8(PHASER_ADDR_DUC_STB, 0) self.write8(PHASER_ADDR_DUC_STB, 0)
@kernel @kernel
def spi_cfg(self, select, div, end, clk_phase=0, clk_polarity=0, def spi_cfg(self, select, div, end, clk_phase=0, clk_polarity=0,
half_duplex=0, lsb_first=0, offline=0, length=8): half_duplex=0, lsb_first=0, offline=0, length=8):
"""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)
"""
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))
self.write8(PHASER_ADDR_SPI_CFG, self.write8(PHASER_ADDR_SPI_CFG,
(offline << 0) | (end << 1) | (clk_phase << 2) | (offline << 0) | (end << 1) | (clk_phase << 2) |
(clk_polarity << 3) | (half_duplex << 4) | (clk_polarity << 3) | (half_duplex << 4) |
(lsb_first << 5)) (lsb_first << 5))
@kernel @kernel
def spi_write(self, data): def spi_write(self, data):
"""Write 8 bits into the SPI data register and start/continue the
transaction."""
self.write8(PHASER_ADDR_SPI_DATW, data) self.write8(PHASER_ADDR_SPI_DATW, data)
@kernel @kernel
def spi_read(self): def spi_read(self):
"""Read from the SPI input data register."""
return self.read8(PHASER_ADDR_SPI_DATR) return self.read8(PHASER_ADDR_SPI_DATR)
@kernel @kernel
def dac_write(self, addr, data): def dac_write(self, addr, data):
"""Write 16 bit to a DAC register.
:param addr: Register address
:param data: Register data to write
"""
div = 32 # 100 ns min period div = 32 # 100 ns min period
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)
@ -207,6 +338,12 @@ class Phaser:
@kernel @kernel
def dac_read(self, addr, div=32) -> TInt32: def dac_read(self, addr, div=32) -> TInt32:
"""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) 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)
self.spi_write(addr | 0x80) self.spi_write(addr | 0x80)
@ -223,6 +360,11 @@ class Phaser:
@kernel @kernel
def att_write(self, ch, data): def att_write(self, ch, data):
"""Set channel attenuation.
:param ch: RF channel (0 or 1)
:param data: Attenuator data
"""
div = 32 # 30 ns min period div = 32 # 30 ns min period
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_ATT0 << ch, div=div, end=1) self.spi_cfg(select=PHASER_SEL_ATT0 << ch, div=div, end=1)
@ -231,6 +373,13 @@ class Phaser:
@kernel @kernel
def att_read(self, ch) -> TInt32: def att_read(self, ch) -> TInt32:
"""Read current attenuation.
The current attenuation value is read without side effects.
:param ch: RF channel (0 or 1)
:return: Current attenuation
"""
div = 32 div = 32
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_ATT0 << ch, div=div, end=0) self.spi_cfg(select=PHASER_SEL_ATT0 << ch, div=div, end=0)
@ -245,6 +394,12 @@ class Phaser:
@kernel @kernel
def trf_write(self, ch, data, readback=False): def trf_write(self, ch, data, readback=False):
"""Write 32 bits to a TRF upconverter.
:param ch: RF channel (0 or 1)
:param data: Register data (32 bit)
:param readback: Whether to return the read back MISO data
"""
div = 32 # 50 ns min period div = 32 # 50 ns min period
t_xfer = self.core.seconds_to_mu((8 + 1)*div*4*ns) t_xfer = self.core.seconds_to_mu((8 + 1)*div*4*ns)
read = 0 read = 0
@ -269,8 +424,16 @@ class Phaser:
@kernel @kernel
def trf_read(self, ch, addr, cnt_mux_sel=0) -> TInt32: def trf_read(self, ch, addr, cnt_mux_sel=0) -> TInt32:
"""TRF upconverter register read.
:param ch: RF channel (0 or 1)
:param addr: Register address to read
:param cnt_mux_sel: Report VCO counter min frequency
or max frequency
:return: Register data (32 bit)
"""
self.trf_write(ch, 0x80000008 | (addr << 28) | (cnt_mux_sel << 27)) self.trf_write(ch, 0x80000008 | (addr << 28) | (cnt_mux_sel << 27))
# single clk pulse to start readback # single clk pulse with ~LE to start readback
self.spi_cfg(select=0, div=32, end=1, length=1) self.spi_cfg(select=0, div=32, end=1, length=1)
self.spi_write(0) self.spi_write(0)
delay((1 + 1)*32*4*ns) delay((1 + 1)*32*4*ns)
@ -278,11 +441,25 @@ class Phaser:
@kernel @kernel
def set_frequency_mu(self, ch, osc, ftw): def set_frequency_mu(self, ch, osc, ftw):
"""Set Phaser MultiDDS frequency tuning word.
:param ch: RF channel (0 or 1)
:param osc: Oscillator number (0 to 4)
:param ftw: Frequency tuning word (32 bit)
"""
addr = ((self.channel_base + 1 + ch) << 8) | (osc << 1) addr = ((self.channel_base + 1 + ch) << 8) | (osc << 1)
rtio_output(addr, ftw) rtio_output(addr, ftw)
@kernel @kernel
def set_amplitude_phase_mu(self, ch, osc, asf=0x7fff, pow=0, clr=0): def set_amplitude_phase_mu(self, ch, osc, asf=0x7fff, pow=0, clr=0):
"""Set Phaser MultiDDS amplitude, phase offset and accumulator clear.
:param ch: RF channel (0 or 1)
:param osc: Oscillator number (0 to 4)
:param asf: Amplitude (15 bit)
:param pow: Phase offset word (16 bit)
:param clr: Clear the phase accumulator (persistent)
"""
addr = ((self.channel_base + 1 + ch) << 8) | (osc << 1) | 1 addr = ((self.channel_base + 1 + ch) << 8) | (osc << 1) | 1
data = (asf & 0x7fff) | (clr << 15) | (pow << 16) data = (asf & 0x7fff) | (clr << 15) | (pow << 16)
rtio_output(addr, data) rtio_output(addr, data)

View File

@ -30,6 +30,7 @@ class Phaser(Module):
enable_replace=False), enable_replace=False),
rtlink.IInterface(data_width=10)) rtlink.IInterface(data_width=10))
# share a CosSinGen LUT between the two channels
self.submodules.ch0 = DDSChannel() self.submodules.ch0 = DDSChannel()
self.submodules.ch1 = DDSChannel(use_lut=self.ch0.dds.mod.cs.lut) self.submodules.ch1 = DDSChannel(use_lut=self.ch0.dds.mod.cs.lut)
n_channels = 2 n_channels = 2
@ -38,7 +39,7 @@ class Phaser(Module):
body = Signal(n_samples*n_channels*2*n_bits, reset_less=True) body = Signal(n_samples*n_channels*2*n_bits, reset_less=True)
self.sync.rio_phy += [ self.sync.rio_phy += [
If(self.ch0.dds.valid, # & self.ch1.dds.valid, If(self.ch0.dds.valid, # & self.ch1.dds.valid,
# recent sample, ch0, i first # recent:ch0:i as low order in body
Cat(body).eq(Cat(self.ch0.dds.o.i[2:], self.ch0.dds.o.q[2:], Cat(body).eq(Cat(self.ch0.dds.o.i[2:], self.ch0.dds.o.q[2:],
self.ch1.dds.o.i[2:], self.ch1.dds.o.q[2:], self.ch1.dds.o.i[2:], self.ch1.dds.o.q[2:],
body)), body)),
@ -66,7 +67,7 @@ class Phaser(Module):
re_dly = Signal(3) # stage, send, respond re_dly = Signal(3) # stage, send, respond
self.sync.rtio += [ self.sync.rtio += [
header.type.eq(1), # reserved header.type.eq(1), # body type is baseband data
If(self.serializer.stb, If(self.serializer.stb,
self.ch0.dds.stb.eq(1), # synchronize self.ch0.dds.stb.eq(1), # synchronize
self.ch1.dds.stb.eq(1), # synchronize self.ch1.dds.stb.eq(1), # synchronize

View File

@ -130,6 +130,11 @@ RF generation drivers
.. automodule:: artiq.coredevice.basemod_att .. automodule:: artiq.coredevice.basemod_att
:members: :members:
:mod:`artiq.coredevice.phaser` module
+++++++++++++++++++++++++++++++++++++
.. automodule:: artiq.coredevice.phaser
:members:
DAC/ADC drivers DAC/ADC drivers
--------------- ---------------