forked from M-Labs/artiq
phaser: documentation
This commit is contained in:
parent
68bfa04abb
commit
272dc5d36a
@ -1,5 +1,3 @@
|
||||
import numpy as np
|
||||
|
||||
from artiq.language.core import kernel, delay_mu, delay
|
||||
from artiq.coredevice.rtio import rtio_output, rtio_input_data
|
||||
from artiq.language.units import us, ns
|
||||
@ -49,44 +47,86 @@ PHASER_STA_TERM0 = 1 << 3
|
||||
PHASER_STA_TERM1 = 1 << 4
|
||||
PHASER_STA_SPI_IDLE = 1 << 5
|
||||
|
||||
PHASER_DAC_SEL_DUC = 0
|
||||
PHASER_DAC_SEL_TEST = 1
|
||||
|
||||
|
||||
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,
|
||||
core_device="core"):
|
||||
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.
|
||||
|
||||
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.core = dmgr.get(core_device)
|
||||
self.miso_delay = miso_delay
|
||||
# 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
|
||||
|
||||
@kernel
|
||||
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)
|
||||
if board_id != PHASER_BOARD_ID:
|
||||
raise ValueError("invalid board id")
|
||||
delay(20*us)
|
||||
delay(20*us) # slack
|
||||
|
||||
@kernel
|
||||
def write8(self, addr, data):
|
||||
"""Write data to a Phaser FPGA register.
|
||||
"""Write data to FPGA register.
|
||||
|
||||
:param addr: Address to write to.
|
||||
:param data: Data to write.
|
||||
:param addr: Address to write to (7 bit)
|
||||
:param data: Data to write (8 bit)
|
||||
"""
|
||||
rtio_output((self.channel_base << 8) | (addr & 0x7f) | 0x80, data)
|
||||
delay_mu(int64(self.t_frame))
|
||||
|
||||
@kernel
|
||||
def read8(self, addr) -> TInt32:
|
||||
"""Read from Phaser FPGA register.
|
||||
"""Read from FPGA register.
|
||||
|
||||
TODO: untested
|
||||
|
||||
:param addr: Address to read from.
|
||||
:return: The data read.
|
||||
:param addr: Address to read from (7 bit)
|
||||
:return: Data read (8 bit)
|
||||
"""
|
||||
rtio_output((self.channel_base << 8) | (addr & 0x7f), 0)
|
||||
response = rtio_input_data(self.channel_base)
|
||||
@ -94,6 +134,7 @@ class Phaser:
|
||||
|
||||
@kernel
|
||||
def write32(self, addr, data: TInt32):
|
||||
"""Write 32 bit to a sequence of FPGA registers."""
|
||||
for offset in range(4):
|
||||
byte = data >> 24
|
||||
self.write8(addr + offset, byte)
|
||||
@ -101,6 +142,7 @@ class Phaser:
|
||||
|
||||
@kernel
|
||||
def read32(self, addr) -> TInt32:
|
||||
"""Read 32 bit from a sequence of FPGA registers."""
|
||||
data = 0
|
||||
for offset in range(4):
|
||||
data <<= 8
|
||||
@ -110,39 +152,80 @@ class Phaser:
|
||||
|
||||
@kernel
|
||||
def write16(self, addr, data: TInt32):
|
||||
"""Write 16 bit to a sequence of FPGA registers."""
|
||||
self.write8(addr, data >> 8)
|
||||
self.write8(addr + 1, data)
|
||||
|
||||
@kernel
|
||||
def read16(self, addr) -> TInt32:
|
||||
"""Read 16 bit from a sequence of FPGA registers."""
|
||||
return (self.read8(addr) << 8) | self.read8(addr)
|
||||
|
||||
@kernel
|
||||
def set_leds(self, leds):
|
||||
"""Set the front panel LEDs.
|
||||
|
||||
:param leds: LED settings (6 bit)
|
||||
"""
|
||||
self.write8(PHASER_ADDR_LED, leds)
|
||||
|
||||
@kernel
|
||||
def set_fan(self, duty):
|
||||
"""Set the fan duty cycle.
|
||||
|
||||
:param duty: Duty cycle (8 bit)
|
||||
"""
|
||||
self.write8(PHASER_ADDR_FAN, duty)
|
||||
|
||||
@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):
|
||||
"""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,
|
||||
(clk_sel << 0) | (dac_resetb << 1) | (dac_sleep << 2) |
|
||||
(dac_txena << 3) | (trf0_ps << 4) | (trf1_ps << 5) |
|
||||
(att0_rstn << 6) | (att1_rstn << 7))
|
||||
(clk_sel << 0) | (dac_resetb << 1) | (dac_sleep << 2) |
|
||||
(dac_txena << 3) | (trf0_ps << 4) | (trf1_ps << 5) |
|
||||
(att0_rstn << 6) | (att1_rstn << 7))
|
||||
|
||||
@kernel
|
||||
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)
|
||||
|
||||
@kernel
|
||||
def get_crc_err(self):
|
||||
"""Get the frame CRC error counter."""
|
||||
return self.read8(PHASER_ADDR_CRC_ERR)
|
||||
|
||||
@kernel
|
||||
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
|
||||
for addr in range(4):
|
||||
data <<= 8
|
||||
@ -152,6 +235,11 @@ class Phaser:
|
||||
|
||||
@kernel
|
||||
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):
|
||||
byte = data >> 24
|
||||
self.write8(PHASER_ADDR_DAC0_TEST + (ch << 4) + addr, byte)
|
||||
@ -159,41 +247,84 @@ class Phaser:
|
||||
|
||||
@kernel
|
||||
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),
|
||||
(clr << 0) | (clr_once << 1) | (select << 2))
|
||||
(clr << 0) | (clr_once << 1) | (select << 2))
|
||||
|
||||
@kernel
|
||||
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)
|
||||
|
||||
@kernel
|
||||
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)
|
||||
|
||||
@kernel
|
||||
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)
|
||||
|
||||
@kernel
|
||||
def spi_cfg(self, select, div, end, clk_phase=0, clk_polarity=0,
|
||||
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_DIVLEN, (div - 2 >> 3) | (length - 1 << 5))
|
||||
self.write8(PHASER_ADDR_SPI_CFG,
|
||||
(offline << 0) | (end << 1) | (clk_phase << 2) |
|
||||
(clk_polarity << 3) | (half_duplex << 4) |
|
||||
(lsb_first << 5))
|
||||
(offline << 0) | (end << 1) | (clk_phase << 2) |
|
||||
(clk_polarity << 3) | (half_duplex << 4) |
|
||||
(lsb_first << 5))
|
||||
|
||||
@kernel
|
||||
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)
|
||||
|
||||
@kernel
|
||||
def spi_read(self):
|
||||
"""Read from the SPI input data register."""
|
||||
return self.read8(PHASER_ADDR_SPI_DATR)
|
||||
|
||||
@kernel
|
||||
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
|
||||
t_xfer = self.core.seconds_to_mu((8 + 1)*div*4*ns)
|
||||
self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=0)
|
||||
@ -207,6 +338,12 @@ class Phaser:
|
||||
|
||||
@kernel
|
||||
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)
|
||||
self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=0)
|
||||
self.spi_write(addr | 0x80)
|
||||
@ -223,6 +360,11 @@ class Phaser:
|
||||
|
||||
@kernel
|
||||
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
|
||||
t_xfer = self.core.seconds_to_mu((8 + 1)*div*4*ns)
|
||||
self.spi_cfg(select=PHASER_SEL_ATT0 << ch, div=div, end=1)
|
||||
@ -231,6 +373,13 @@ class Phaser:
|
||||
|
||||
@kernel
|
||||
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
|
||||
t_xfer = self.core.seconds_to_mu((8 + 1)*div*4*ns)
|
||||
self.spi_cfg(select=PHASER_SEL_ATT0 << ch, div=div, end=0)
|
||||
@ -245,6 +394,12 @@ class Phaser:
|
||||
|
||||
@kernel
|
||||
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
|
||||
t_xfer = self.core.seconds_to_mu((8 + 1)*div*4*ns)
|
||||
read = 0
|
||||
@ -269,8 +424,16 @@ class Phaser:
|
||||
|
||||
@kernel
|
||||
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))
|
||||
# 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_write(0)
|
||||
delay((1 + 1)*32*4*ns)
|
||||
@ -278,11 +441,25 @@ class Phaser:
|
||||
|
||||
@kernel
|
||||
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)
|
||||
rtio_output(addr, ftw)
|
||||
|
||||
@kernel
|
||||
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
|
||||
data = (asf & 0x7fff) | (clr << 15) | (pow << 16)
|
||||
rtio_output(addr, data)
|
||||
|
@ -30,6 +30,7 @@ class Phaser(Module):
|
||||
enable_replace=False),
|
||||
rtlink.IInterface(data_width=10))
|
||||
|
||||
# share a CosSinGen LUT between the two channels
|
||||
self.submodules.ch0 = DDSChannel()
|
||||
self.submodules.ch1 = DDSChannel(use_lut=self.ch0.dds.mod.cs.lut)
|
||||
n_channels = 2
|
||||
@ -38,7 +39,7 @@ class Phaser(Module):
|
||||
body = Signal(n_samples*n_channels*2*n_bits, reset_less=True)
|
||||
self.sync.rio_phy += [
|
||||
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:],
|
||||
self.ch1.dds.o.i[2:], self.ch1.dds.o.q[2:],
|
||||
body)),
|
||||
@ -66,7 +67,7 @@ class Phaser(Module):
|
||||
|
||||
re_dly = Signal(3) # stage, send, respond
|
||||
self.sync.rtio += [
|
||||
header.type.eq(1), # reserved
|
||||
header.type.eq(1), # body type is baseband data
|
||||
If(self.serializer.stb,
|
||||
self.ch0.dds.stb.eq(1), # synchronize
|
||||
self.ch1.dds.stb.eq(1), # synchronize
|
||||
|
@ -130,6 +130,11 @@ RF generation drivers
|
||||
.. automodule:: artiq.coredevice.basemod_att
|
||||
:members:
|
||||
|
||||
:mod:`artiq.coredevice.phaser` module
|
||||
+++++++++++++++++++++++++++++++++++++
|
||||
|
||||
.. automodule:: artiq.coredevice.phaser
|
||||
:members:
|
||||
|
||||
DAC/ADC drivers
|
||||
---------------
|
||||
|
Loading…
Reference in New Issue
Block a user