mirror of https://github.com/m-labs/artiq.git
Merge pull request #1962 from quartiq/miqro
Support MIQRO mode for Phaser
This commit is contained in:
commit
e5c621751f
|
@ -9,7 +9,8 @@ Unreleased
|
||||||
Highlights:
|
Highlights:
|
||||||
|
|
||||||
* Implemented Phaser-servo. This requires recent gateware on Phaser.
|
* Implemented Phaser-servo. This requires recent gateware on Phaser.
|
||||||
|
* Implemented Phaser-MIQRO support. This requires the Phaser MIQRO gateware
|
||||||
|
variant.
|
||||||
|
|
||||||
ARTIQ-7
|
ARTIQ-7
|
||||||
-------
|
-------
|
||||||
|
|
|
@ -9,6 +9,10 @@ from artiq.coredevice.trf372017 import TRF372017
|
||||||
|
|
||||||
|
|
||||||
PHASER_BOARD_ID = 19
|
PHASER_BOARD_ID = 19
|
||||||
|
|
||||||
|
PHASER_GW_BASE = 1
|
||||||
|
PHASER_GW_MIQRO = 2
|
||||||
|
|
||||||
PHASER_ADDR_BOARD_ID = 0x00
|
PHASER_ADDR_BOARD_ID = 0x00
|
||||||
PHASER_ADDR_HW_REV = 0x01
|
PHASER_ADDR_HW_REV = 0x01
|
||||||
PHASER_ADDR_GW_REV = 0x02
|
PHASER_ADDR_GW_REV = 0x02
|
||||||
|
@ -47,6 +51,12 @@ PHASER_ADDR_SERVO_CFG1 = 0x31
|
||||||
# 0x32 - 0x71 servo coefficients + offset data
|
# 0x32 - 0x71 servo coefficients + offset data
|
||||||
PHASER_ADDR_SERVO_DATA_BASE = 0x32
|
PHASER_ADDR_SERVO_DATA_BASE = 0x32
|
||||||
|
|
||||||
|
# 0x72 - 0x78 Miqro channel profile/window memories
|
||||||
|
PHASER_ADDR_MIQRO_MEM_ADDR = 0x72
|
||||||
|
PHASER_ADDR_MIQRO_MEM_DATA = 0x74
|
||||||
|
|
||||||
|
# Miqro profile memory select
|
||||||
|
PHASER_MIQRO_SEL_PROFILE = 1 << 14
|
||||||
|
|
||||||
PHASER_SEL_DAC = 1 << 0
|
PHASER_SEL_DAC = 1 << 0
|
||||||
PHASER_SEL_TRF0 = 1 << 1
|
PHASER_SEL_TRF0 = 1 << 1
|
||||||
|
@ -78,6 +88,26 @@ 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 RTIO PHY and the Phaser gateware come in different modes
|
||||||
|
that have different features. Phaser mode and coredevice PHY mode are both
|
||||||
|
selected at their respective gateware compile-time and need to match.
|
||||||
|
|
||||||
|
=============== ============== ===================================
|
||||||
|
Phaser gateware Coredevice PHY Features per :class:`PhaserChannel`
|
||||||
|
=============== ============== ===================================
|
||||||
|
Base <= v0.5 Base Base (5 :class:`PhaserOscillator`)
|
||||||
|
Base >= v0.6 Base Base + Servo
|
||||||
|
Miqro >= v0.6 Miqro :class:`Miqro`
|
||||||
|
=============== ============== ===================================
|
||||||
|
|
||||||
|
The coredevice driver (this class and :class:`PhaserChannel`) exposes
|
||||||
|
the superset of all functionality regardless of the Coredevice RTIO PHY
|
||||||
|
or Phaser gateware modes. This is to evade type unification limitations.
|
||||||
|
Features absent in Coredevice PHY/Phaser gateware will not work and
|
||||||
|
should not be accessed.
|
||||||
|
|
||||||
|
**Base mode**
|
||||||
|
|
||||||
The coredevice produces 2 IQ (in-phase and quadrature) data streams with 25
|
The coredevice produces 2 IQ (in-phase and quadrature) data streams with 25
|
||||||
MS/s and 14 bit per quadrature. Each data stream supports 5 independent
|
MS/s and 14 bit per quadrature. Each data stream supports 5 independent
|
||||||
numerically controlled IQ oscillators (NCOs, DDSs with 32 bit frequency, 16
|
numerically controlled IQ oscillators (NCOs, DDSs with 32 bit frequency, 16
|
||||||
|
@ -108,6 +138,14 @@ class Phaser:
|
||||||
absolute phase with respect to other RTIO input and output events
|
absolute phase with respect to other RTIO input and output events
|
||||||
(see `get_next_frame_mu()`).
|
(see `get_next_frame_mu()`).
|
||||||
|
|
||||||
|
**Miqro mode**
|
||||||
|
|
||||||
|
See :class:`Miqro`
|
||||||
|
|
||||||
|
Here the DAC operates in 4x interpolation.
|
||||||
|
|
||||||
|
**Analog flow**
|
||||||
|
|
||||||
The four analog DAC outputs are passed through anti-aliasing filters.
|
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
|
In the baseband variant, the even/in-phase DAC channels feed 31.5 dB range
|
||||||
|
@ -125,6 +163,8 @@ class Phaser:
|
||||||
configured through a shared SPI bus that is accessed and controlled via
|
configured through a shared SPI bus that is accessed and controlled via
|
||||||
FPGA registers.
|
FPGA registers.
|
||||||
|
|
||||||
|
**Servo**
|
||||||
|
|
||||||
Each phaser output channel features a servo to control the RF output amplitude
|
Each phaser output channel features a servo to control the RF output amplitude
|
||||||
using feedback from an ADC. The servo consists of a first order IIR (infinite
|
using feedback from an ADC. The servo consists of a first order IIR (infinite
|
||||||
impulse response) filter fed by the ADC and a multiplier that scales the I
|
impulse response) filter fed by the ADC and a multiplier that scales the I
|
||||||
|
@ -203,6 +243,7 @@ class Phaser:
|
||||||
self.clk_sel = clk_sel
|
self.clk_sel = clk_sel
|
||||||
self.tune_fifo_offset = tune_fifo_offset
|
self.tune_fifo_offset = tune_fifo_offset
|
||||||
self.sync_dly = sync_dly
|
self.sync_dly = sync_dly
|
||||||
|
self.gw_rev = -1 # discovered in init()
|
||||||
|
|
||||||
self.dac_mmap = DAC34H84(dac).get_mmap()
|
self.dac_mmap = DAC34H84(dac).get_mmap()
|
||||||
|
|
||||||
|
@ -226,9 +267,9 @@ class Phaser:
|
||||||
delay(.1*ms) # slack
|
delay(.1*ms) # slack
|
||||||
is_baseband = hw_rev & PHASER_HW_REV_VARIANT
|
is_baseband = hw_rev & PHASER_HW_REV_VARIANT
|
||||||
|
|
||||||
gw_rev = self.read8(PHASER_ADDR_GW_REV)
|
self.gw_rev = self.read8(PHASER_ADDR_GW_REV)
|
||||||
if debug:
|
if debug:
|
||||||
print("gw_rev:", gw_rev)
|
print("gw_rev:", self.gw_rev)
|
||||||
self.core.break_realtime()
|
self.core.break_realtime()
|
||||||
delay(.1*ms) # slack
|
delay(.1*ms) # slack
|
||||||
|
|
||||||
|
@ -346,36 +387,40 @@ class Phaser:
|
||||||
if channel.get_att_mu() != 0x5a:
|
if channel.get_att_mu() != 0x5a:
|
||||||
raise ValueError("attenuator test failed")
|
raise ValueError("attenuator test failed")
|
||||||
delay(.1*ms)
|
delay(.1*ms)
|
||||||
channel.set_att_mu(0x00) # minimum attenuation
|
channel.set_att_mu(0x00) # maximum attenuation
|
||||||
|
|
||||||
channel.set_servo(profile=0, enable=0, hold=1)
|
channel.set_servo(profile=0, enable=0, hold=1)
|
||||||
|
|
||||||
# test oscillators and DUC
|
if self.gw_rev == PHASER_GW_BASE:
|
||||||
for i in range(len(channel.oscillator)):
|
# test oscillators and DUC
|
||||||
oscillator = channel.oscillator[i]
|
for i in range(len(channel.oscillator)):
|
||||||
asf = 0
|
oscillator = channel.oscillator[i]
|
||||||
if i == 0:
|
asf = 0
|
||||||
asf = 0x7fff
|
if i == 0:
|
||||||
# 6pi/4 phase
|
asf = 0x7fff
|
||||||
oscillator.set_amplitude_phase_mu(asf=asf, pow=0xc000, clr=1)
|
# 6pi/4 phase
|
||||||
|
oscillator.set_amplitude_phase_mu(asf=asf, pow=0xc000, clr=1)
|
||||||
|
delay(1*us)
|
||||||
|
# 3pi/4
|
||||||
|
channel.set_duc_phase_mu(0x6000)
|
||||||
|
channel.set_duc_cfg(select=0, clr=1)
|
||||||
|
self.duc_stb()
|
||||||
|
delay(.1*ms) # settle link, pipeline and impulse response
|
||||||
|
data = channel.get_dac_data()
|
||||||
delay(1*us)
|
delay(1*us)
|
||||||
# 3pi/4
|
channel.oscillator[0].set_amplitude_phase_mu(asf=0, pow=0xc000,
|
||||||
channel.set_duc_phase_mu(0x6000)
|
clr=1)
|
||||||
channel.set_duc_cfg(select=0, clr=1)
|
delay(.1*ms)
|
||||||
self.duc_stb()
|
sqrt2 = 0x5a81 # 0x7fff/sqrt(2)
|
||||||
delay(.1*ms) # settle link, pipeline and impulse response
|
data_i = data & 0xffff
|
||||||
data = channel.get_dac_data()
|
data_q = (data >> 16) & 0xffff
|
||||||
delay(1*us)
|
# allow ripple
|
||||||
channel.oscillator[0].set_amplitude_phase_mu(asf=0, pow=0xc000,
|
if (data_i < sqrt2 - 30 or data_i > sqrt2 or
|
||||||
clr=1)
|
abs(data_i - data_q) > 2):
|
||||||
delay(.1*ms)
|
raise ValueError("DUC+oscillator phase/amplitude test failed")
|
||||||
sqrt2 = 0x5a81 # 0x7fff/sqrt(2)
|
|
||||||
data_i = data & 0xffff
|
if self.gw_rev == PHASER_GW_MIQRO:
|
||||||
data_q = (data >> 16) & 0xffff
|
channel.miqro.reset()
|
||||||
# allow ripple
|
|
||||||
if (data_i < sqrt2 - 30 or data_i > sqrt2 or
|
|
||||||
abs(data_i - data_q) > 2):
|
|
||||||
raise ValueError("DUC+oscillator phase/amplitude test failed")
|
|
||||||
|
|
||||||
if is_baseband:
|
if is_baseband:
|
||||||
continue
|
continue
|
||||||
|
@ -782,6 +827,8 @@ class Phaser:
|
||||||
if good & (1 << o):
|
if good & (1 << o):
|
||||||
sum += o
|
sum += o
|
||||||
count += 1
|
count += 1
|
||||||
|
if count == 0:
|
||||||
|
raise ValueError("no good fifo offset")
|
||||||
best = ((sum // count) + offset) % 8
|
best = ((sum // count) + offset) % 8
|
||||||
self.dac_write(0x09, (config9 & 0x1fff) | (best << 13))
|
self.dac_write(0x09, (config9 & 0x1fff) | (best << 13))
|
||||||
return best
|
return best
|
||||||
|
@ -792,8 +839,9 @@ class PhaserChannel:
|
||||||
|
|
||||||
A Phaser channel contains:
|
A Phaser channel contains:
|
||||||
|
|
||||||
* multiple oscillators (in the coredevice phy),
|
* multiple :class:`PhaserOscillator` (in the coredevice phy),
|
||||||
* an interpolation chain and digital upconverter (DUC) on Phaser,
|
* an interpolation chain and digital upconverter (DUC) on Phaser,
|
||||||
|
* a :class:`Miqro` instance on Phaser,
|
||||||
* several channel-specific settings in the DAC:
|
* several channel-specific settings in the DAC:
|
||||||
|
|
||||||
* quadrature modulation compensation QMC
|
* quadrature modulation compensation QMC
|
||||||
|
@ -805,6 +853,7 @@ class PhaserChannel:
|
||||||
Attributes:
|
Attributes:
|
||||||
|
|
||||||
* :attr:`oscillator`: List of five :class:`PhaserOscillator`.
|
* :attr:`oscillator`: List of five :class:`PhaserOscillator`.
|
||||||
|
* :attr:`miqro`: A :class:`Miqro`.
|
||||||
|
|
||||||
.. note:: The amplitude sum of the oscillators must be less than one to
|
.. 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
|
avoid clipping or overflow. If any of the DDS or DUC frequencies are
|
||||||
|
@ -817,6 +866,8 @@ class PhaserChannel:
|
||||||
changes in oscillator parameters, the overshoot can lead to clipping
|
changes in oscillator parameters, the overshoot can lead to clipping
|
||||||
or overflow after the interpolation. Either band-limit any changes
|
or overflow after the interpolation. Either band-limit any changes
|
||||||
in the oscillator parameters or back off the amplitude sufficiently.
|
in the oscillator parameters or back off the amplitude sufficiently.
|
||||||
|
Miqro is not affected by this. But both the oscillators and Miqro can
|
||||||
|
be affected by intrinsic overshoot of the interpolator on the DAC.
|
||||||
"""
|
"""
|
||||||
kernel_invariants = {"index", "phaser", "trf_mmap"}
|
kernel_invariants = {"index", "phaser", "trf_mmap"}
|
||||||
|
|
||||||
|
@ -826,6 +877,7 @@ class PhaserChannel:
|
||||||
self.trf_mmap = TRF372017(trf).get_mmap()
|
self.trf_mmap = TRF372017(trf).get_mmap()
|
||||||
|
|
||||||
self.oscillator = [PhaserOscillator(self, osc) for osc in range(5)]
|
self.oscillator = [PhaserOscillator(self, osc) for osc in range(5)]
|
||||||
|
self.miqro = Miqro(self)
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def get_dac_data(self) -> TInt32:
|
def get_dac_data(self) -> TInt32:
|
||||||
|
@ -1139,7 +1191,7 @@ class PhaserChannel:
|
||||||
for data in [b0, b1, a1, offset]:
|
for data in [b0, b1, a1, offset]:
|
||||||
self.phaser.write16(addr, data)
|
self.phaser.write16(addr, data)
|
||||||
addr += 2
|
addr += 2
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def set_iir(self, profile, kp, ki=0., g=0., x_offset=0., y_offset=0.):
|
def set_iir(self, profile, kp, ki=0., g=0., x_offset=0., y_offset=0.):
|
||||||
"""Set servo profile IIR coefficients.
|
"""Set servo profile IIR coefficients.
|
||||||
|
@ -1149,8 +1201,8 @@ class PhaserChannel:
|
||||||
Gains are given in units of output full per scale per input full scale.
|
Gains are given in units of output full per scale per input full scale.
|
||||||
|
|
||||||
.. note:: Due to inherent constraints of the fixed point datatypes and IIR
|
.. note:: Due to inherent constraints of the fixed point datatypes and IIR
|
||||||
filters, the ``x_offset`` (setpoint) resolution depends on the selected gains.
|
filters, the ``x_offset`` (setpoint) resolution depends on the selected
|
||||||
Low ``ki`` gains will lead to a low ``x_offset`` resolution.
|
gains. Low ``ki`` gains will lead to a low ``x_offset`` resolution.
|
||||||
|
|
||||||
The transfer function is (up to time discretization and
|
The transfer function is (up to time discretization and
|
||||||
coefficient quantization errors):
|
coefficient quantization errors):
|
||||||
|
@ -1269,3 +1321,305 @@ class PhaserOscillator:
|
||||||
raise ValueError("amplitude out of bounds")
|
raise ValueError("amplitude out of bounds")
|
||||||
pow = int32(round(phase*(1 << 16)))
|
pow = int32(round(phase*(1 << 16)))
|
||||||
self.set_amplitude_phase_mu(asf, pow, clr)
|
self.set_amplitude_phase_mu(asf, pow, clr)
|
||||||
|
|
||||||
|
|
||||||
|
class Miqro:
|
||||||
|
"""
|
||||||
|
Miqro pulse generator.
|
||||||
|
|
||||||
|
A Miqro instance represents one RF output. The DSP components are fully
|
||||||
|
contained in the Phaser gateware. The output is generated by with
|
||||||
|
the following data flow:
|
||||||
|
|
||||||
|
**Oscillators**
|
||||||
|
|
||||||
|
* There are n_osc = 16 oscillators with oscillator IDs 0..n_osc-1.
|
||||||
|
* Each oscillator outputs one tone at any given time
|
||||||
|
|
||||||
|
* I/Q (quadrature, a.k.a. complex) 2x16 bit signed data
|
||||||
|
at tau = 4 ns sample intervals, 250 MS/s, Nyquist 125 MHz, bandwidth 200 MHz
|
||||||
|
(from f = -100..+100 MHz, taking into account the interpolation anti-aliasing
|
||||||
|
filters in subsequent interpolators),
|
||||||
|
* 32 bit frequency (f) resolution (~ 1/16 Hz),
|
||||||
|
* 16 bit unsigned amplitude (a) resolution
|
||||||
|
* 16 bit phase offset (p) resolution
|
||||||
|
|
||||||
|
* The output phase p' of each oscillator at time t (boot/reset/initialization of the
|
||||||
|
device at t=0) is then p' = f*t + p (mod 1 turn) where f and p are the (currently
|
||||||
|
active) profile frequency and phase offset.
|
||||||
|
* Note: The terms "phase coherent" and "phase tracking" are defined to refer to this
|
||||||
|
choice of oscillator output phase p'. Note that the phase offset p is not relative to
|
||||||
|
(on top of previous phase/profiles/oscillator history).
|
||||||
|
It is "absolute" in the sense that frequency f and phase offset p fully determine
|
||||||
|
oscillator output phase p' at time t. This is unlike typical DDS behavior.
|
||||||
|
* Frequency, phase, and amplitude of each oscillator are configurable by selecting one of
|
||||||
|
n_profile = 32 profiles 0..n_profile-1. This selection is fast and can be done for
|
||||||
|
each pulse. The phase coherence defined above is guaranteed for each
|
||||||
|
profile individually.
|
||||||
|
* Note: one profile per oscillator (usually profile index 0) should be reserved
|
||||||
|
for the NOP (no operation, identity) profile, usually with zero amplitude.
|
||||||
|
* Data for each profile for each oscillator can be configured
|
||||||
|
individually. Storing profile data should be considered "expensive".
|
||||||
|
* Note: The annotation that some operation is "expensive" does not mean it is
|
||||||
|
impossible, just that it may take a significant amount of time and
|
||||||
|
resources to execute such that it may be impractical when used often or
|
||||||
|
during fast pulse sequences. They are intended for use in calibration and
|
||||||
|
initialization.
|
||||||
|
|
||||||
|
**Summation**
|
||||||
|
|
||||||
|
* The oscillator outputs are added together (wrapping addition).
|
||||||
|
* The user must ensure that the sum of oscillators outputs does not exceed the
|
||||||
|
data range. In general that means that the sum of the amplitudes must not
|
||||||
|
exceed one.
|
||||||
|
|
||||||
|
**Shaper**
|
||||||
|
|
||||||
|
* The summed complex output stream is then multiplied with a the complex-valued
|
||||||
|
output of a triggerable shaper.
|
||||||
|
* Triggering the shaper corresponds to passing a pulse from all oscillators to
|
||||||
|
the RF output.
|
||||||
|
* Selected profiles become active simultaneously (on the same output sample) when
|
||||||
|
triggering the shaper with the first shaper output sample.
|
||||||
|
* The shaper reads (replays) window samples from a memory of size n_window = 1 << 10.
|
||||||
|
* The window memory can be segmented by choosing different start indices
|
||||||
|
to support different windows.
|
||||||
|
* Each window memory segment starts with a header determining segment
|
||||||
|
length and interpolation parameters.
|
||||||
|
* The window samples are interpolated by a factor (rate change) between 1 and
|
||||||
|
r = 1 << 12.
|
||||||
|
* The interpolation order is constant, linear, quadratic, or cubic. This
|
||||||
|
corresponds to interpolation modes from rectangular window (1st order CIC)
|
||||||
|
or zero order hold) to Parzen window (4th order CIC or cubic spline).
|
||||||
|
* This results in support for single shot pulse lengths (envelope support) between
|
||||||
|
tau and a bit more than r * n_window * tau = (1 << 12 + 10) tau ~ 17 ms.
|
||||||
|
* Windows can be configured to be head-less and/or tail-less, meaning, they
|
||||||
|
do not feed zero-amplitude samples into the shaper before and after
|
||||||
|
each window respectively. This is used to implement pulses with arbitrary
|
||||||
|
length or CW output.
|
||||||
|
|
||||||
|
**Overall properties**
|
||||||
|
|
||||||
|
* The DAC may upconvert the signal by applying a frequency offset f1 with
|
||||||
|
phase p1.
|
||||||
|
* In the Upconverter Phaser variant, the analog quadrature upconverter
|
||||||
|
applies another frequency of f2 and phase p2.
|
||||||
|
* The resulting phase of the signal from one oscillator at the SMA output is
|
||||||
|
(f + f1 + f2)*t + p + s(t - t0) + p1 + p2 (mod 1 turn)
|
||||||
|
where s(t - t0) is the phase of the interpolated
|
||||||
|
shaper output, and t0 is the trigger time (fiducial of the shaper).
|
||||||
|
Unsurprisingly the frequency is the derivative of the phase.
|
||||||
|
* Group delays between pulse parameter updates are matched across oscillators,
|
||||||
|
shapers, and channels.
|
||||||
|
* The minimum time to change profiles and phase offsets is ~128 ns (estimate, TBC).
|
||||||
|
This is the minimum pulse interval.
|
||||||
|
The sustained pulse rate of the RTIO PHY/Fastlink is one pulse per Fastlink frame
|
||||||
|
(may be increased, TBC).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, channel):
|
||||||
|
self.channel = channel
|
||||||
|
self.base_addr = (self.channel.phaser.channel_base + 1 +
|
||||||
|
self.channel.index) << 8
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def reset(self):
|
||||||
|
"""Establish no-output profiles and no-output window and execute them.
|
||||||
|
|
||||||
|
This establishes the first profile (index 0) on all oscillators as zero
|
||||||
|
amplitude, creates a trivial window (one sample with zero amplitude,
|
||||||
|
minimal interpolation), and executes a corresponding pulse.
|
||||||
|
"""
|
||||||
|
for osc in range(16):
|
||||||
|
self.set_profile_mu(osc, profile=0, ftw=0, asf=0)
|
||||||
|
delay(20*us)
|
||||||
|
self.set_window_mu(start=0, iq=[0], order=0)
|
||||||
|
self.pulse(window=0, profiles=[0])
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def set_profile_mu(self, oscillator, profile, ftw, asf, pow_=0):
|
||||||
|
"""Store an oscillator profile (machine units).
|
||||||
|
|
||||||
|
:param oscillator: Oscillator index (0 to 15)
|
||||||
|
:param profile: Profile index (0 to 31)
|
||||||
|
:param ftw: Frequency tuning word (32 bit signed integer on a 250 MHz clock)
|
||||||
|
:param asf: Amplitude scale factor (16 bit unsigned integer)
|
||||||
|
:param pow_: Phase offset word (16 bit integer)
|
||||||
|
"""
|
||||||
|
if oscillator >= 16:
|
||||||
|
raise ValueError("invalid oscillator index")
|
||||||
|
if profile >= 32:
|
||||||
|
raise ValueError("invalid profile index")
|
||||||
|
self.channel.phaser.write16(PHASER_ADDR_MIQRO_MEM_ADDR,
|
||||||
|
(self.channel.index << 15) | PHASER_MIQRO_SEL_PROFILE |
|
||||||
|
(oscillator << 6) | (profile << 1))
|
||||||
|
self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, ftw)
|
||||||
|
self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA,
|
||||||
|
(asf & 0xffff) | (pow_ << 16))
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def set_profile(self, oscillator, profile, frequency, amplitude, phase=0.):
|
||||||
|
"""Store an oscillator profile.
|
||||||
|
|
||||||
|
:param oscillator: Oscillator index (0 to 15)
|
||||||
|
:param profile: Profile index (0 to 31)
|
||||||
|
:param frequency: Frequency in Hz (passband -100 to 100 MHz).
|
||||||
|
Interpreted in the Nyquist sense, i.e. aliased.
|
||||||
|
:param amplitude: Amplitude in units of full scale (0. to 1.)
|
||||||
|
:param phase: Phase in turns. See :class:`Miqro` for a definition of
|
||||||
|
phase in this context.
|
||||||
|
:return: The quantized 32 bit frequency tuning word
|
||||||
|
"""
|
||||||
|
ftw = int32(round(frequency*((1 << 30)/(62.5*MHz))))
|
||||||
|
asf = int32(round(amplitude*0xffff))
|
||||||
|
if asf < 0 or asf > 0xffff:
|
||||||
|
raise ValueError("amplitude out of bounds")
|
||||||
|
pow_ = int32(round(phase*(1 << 16)))
|
||||||
|
self.set_profile_mu(oscillator, profile, ftw, asf, pow_)
|
||||||
|
return ftw
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def set_window_mu(self, start, iq, rate=1, shift=0, order=3, head=1, tail=1):
|
||||||
|
"""Store a window segment (machine units)
|
||||||
|
|
||||||
|
:param start: Window start address (0 to 0x3ff)
|
||||||
|
:param iq: List of IQ window samples. Each window sample is an integer
|
||||||
|
containing the signed I part in the 16 LSB and the signed Q part in
|
||||||
|
the 16 MSB. The maximum window length is 0x3fe. The user must
|
||||||
|
ensure that this window does not overlap with other windows in the
|
||||||
|
memory.
|
||||||
|
:param rate: Interpolation rate change (1 to 1 << 12)
|
||||||
|
:param shift: Interpolator amplitude gain compensation in powers of 2 (0 to 63)
|
||||||
|
:param order: Interpolation order from 0 (corresponding to
|
||||||
|
constant/rectangular window/zero-order-hold/1st order CIC interpolation)
|
||||||
|
to 3 (corresponding to cubic/Parzen window/4th order CIC interpolation)
|
||||||
|
:param head: Update the interpolator settings and clear its state at the start
|
||||||
|
of the window. This also implies starting the envelope from zero.
|
||||||
|
:param tail: Feed zeros into the interpolator after the window samples.
|
||||||
|
In the absence of further pulses this will return the output envelope
|
||||||
|
to zero with the chosen interpolation.
|
||||||
|
:return: Next available window memory address after this segment.
|
||||||
|
"""
|
||||||
|
if start >= 1 << 10:
|
||||||
|
raise ValueError("start out of bounds")
|
||||||
|
if len(iq) >= 1 << 10:
|
||||||
|
raise ValueError("window length out of bounds")
|
||||||
|
if rate < 1 or rate > 1 << 12:
|
||||||
|
raise ValueError("rate out of bounds")
|
||||||
|
if shift > 0x3f:
|
||||||
|
raise ValueError("shift out of bounds")
|
||||||
|
if order > 3:
|
||||||
|
raise ValueError("order out of bounds")
|
||||||
|
self.channel.phaser.write16(PHASER_ADDR_MIQRO_MEM_ADDR,
|
||||||
|
(self.channel.index << 15) | start)
|
||||||
|
self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA,
|
||||||
|
(len(iq) & 0x3ff) |
|
||||||
|
((rate - 1) << 10) |
|
||||||
|
(shift << 22) |
|
||||||
|
(order << 28) |
|
||||||
|
((head & 1) << 30) |
|
||||||
|
((tail & 1) << 31)
|
||||||
|
)
|
||||||
|
for iqi in iq:
|
||||||
|
self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, iqi)
|
||||||
|
delay(20*us) # slack for long windows
|
||||||
|
return (start + 1 + len(iq)) & 0x3ff
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def set_window(self, start, iq, period=4*ns, order=3, head=1, tail=1):
|
||||||
|
"""Store a window segment
|
||||||
|
|
||||||
|
:param start: Window start address (0 to 0x3ff)
|
||||||
|
:param iq: List of IQ window samples. Each window sample is a pair of
|
||||||
|
two float numbers -1 to 1, one for each I and Q in units of full scale.
|
||||||
|
The maximum window length is 0x3fe. The user must ensure that this window
|
||||||
|
does not overlap with other windows in the memory.
|
||||||
|
:param period: Desired window sample period in SI units (4*ns to (4 << 12)*ns).
|
||||||
|
:param order: Interpolation order from 0 (corresponding to
|
||||||
|
constant/zero-order-hold/1st order CIC interpolation) to 3 (corresponding
|
||||||
|
to cubic/Parzen/4th order CIC interpolation)
|
||||||
|
:param head: Update the interpolator settings and clear its state at the start
|
||||||
|
of the window. This also implies starting the envelope from zero.
|
||||||
|
:param tail: Feed zeros into the interpolator after the window samples.
|
||||||
|
In the absence of further pulses this will return the output envelope
|
||||||
|
to zero with the chosen interpolation.
|
||||||
|
:return: Actual sample period in SI units
|
||||||
|
"""
|
||||||
|
rate = int32(round(period/(4*ns)))
|
||||||
|
gain = 1.
|
||||||
|
for _ in range(order):
|
||||||
|
gain *= rate
|
||||||
|
shift = 0
|
||||||
|
while gain >= 2.:
|
||||||
|
shift += 1
|
||||||
|
gain *= .5
|
||||||
|
scale = ((1 << 15) - 1)/gain
|
||||||
|
iq_mu = [
|
||||||
|
(int32(round(iqi[0]*scale)) & 0xffff) |
|
||||||
|
(int32(round(iqi[1]*scale)) << 16)
|
||||||
|
for iqi in iq
|
||||||
|
]
|
||||||
|
self.set_window_mu(start, iq_mu, rate, shift, order, head, tail)
|
||||||
|
return (len(iq) + order)*rate*4*ns
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def encode(self, window, profiles, data):
|
||||||
|
"""Encode window and profile selection
|
||||||
|
|
||||||
|
:param window: Window start address (0 to 0x3ff)
|
||||||
|
:param profiles: List of profile indices for the oscillators. Maximum
|
||||||
|
length 16. Unused oscillators will be set to profile 0.
|
||||||
|
:param data: List of integers to store the encoded data words into.
|
||||||
|
Unused entries will remain untouched. Must contain at least three
|
||||||
|
lements if all oscillators are used and should be initialized to
|
||||||
|
zeros.
|
||||||
|
:return: Number of words from `data` used.
|
||||||
|
"""
|
||||||
|
if len(profiles) > 16:
|
||||||
|
raise ValueError("too many oscillators")
|
||||||
|
if window > 0x3ff:
|
||||||
|
raise ValueError("window start out of bounds")
|
||||||
|
data[0] = window
|
||||||
|
word = 0
|
||||||
|
idx = 10
|
||||||
|
for profile in profiles:
|
||||||
|
if profile > 0x1f:
|
||||||
|
raise ValueError("profile out of bounds")
|
||||||
|
if idx > 32 - 5:
|
||||||
|
word += 1
|
||||||
|
idx = 0
|
||||||
|
data[word] |= profile << idx
|
||||||
|
idx += 5
|
||||||
|
return word + 1
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def pulse_mu(self, data):
|
||||||
|
"""Emit a pulse (encoded)
|
||||||
|
|
||||||
|
The pulse fiducial timing resolution is 4 ns.
|
||||||
|
|
||||||
|
:param data: List of up to 3 words containing an encoded MIQRO pulse as
|
||||||
|
returned by :meth:`encode`.
|
||||||
|
"""
|
||||||
|
word = len(data)
|
||||||
|
delay_mu(-8*word) # back shift to align
|
||||||
|
while word > 0:
|
||||||
|
word -= 1
|
||||||
|
delay_mu(8)
|
||||||
|
# final write sets pulse stb
|
||||||
|
rtio_output(self.base_addr + word, data[word])
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def pulse(self, window, profiles):
|
||||||
|
"""Emit a pulse
|
||||||
|
|
||||||
|
This encodes the window and profiles (see :meth:`encode`) and emits them
|
||||||
|
(see :meth:`pulse_mu`).
|
||||||
|
|
||||||
|
:param window: Window start address (0 to 0x3ff)
|
||||||
|
:param profiles: List of profile indices for the oscillators. Maximum
|
||||||
|
length 16. Unused oscillators will select profile 0.
|
||||||
|
"""
|
||||||
|
data = [0, 0, 0]
|
||||||
|
words = self.encode(window, profiles, data)
|
||||||
|
self.pulse_mu(data[:words])
|
||||||
|
|
|
@ -559,6 +559,13 @@ class PeripheralManager:
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def process_phaser(self, rtio_offset, peripheral):
|
def process_phaser(self, rtio_offset, peripheral):
|
||||||
|
mode = peripheral.get("mode", "base")
|
||||||
|
if mode == "miqro":
|
||||||
|
dac = ', "dac": {"pll_m": 16, "pll_n": 3, "interpolation": 2}'
|
||||||
|
n_channels = 3
|
||||||
|
else:
|
||||||
|
dac = ""
|
||||||
|
n_channels = 5
|
||||||
self.gen("""
|
self.gen("""
|
||||||
device_db["{name}"] = {{
|
device_db["{name}"] = {{
|
||||||
"type": "local",
|
"type": "local",
|
||||||
|
@ -566,12 +573,13 @@ class PeripheralManager:
|
||||||
"class": "Phaser",
|
"class": "Phaser",
|
||||||
"arguments": {{
|
"arguments": {{
|
||||||
"channel_base": 0x{channel:06x},
|
"channel_base": 0x{channel:06x},
|
||||||
"miso_delay": 1,
|
"miso_delay": 1{dac}
|
||||||
}}
|
}}
|
||||||
}}""",
|
}}""",
|
||||||
name=self.get_name("phaser"),
|
name=self.get_name("phaser"),
|
||||||
|
dac=dac,
|
||||||
channel=rtio_offset)
|
channel=rtio_offset)
|
||||||
return 5
|
return n_channels
|
||||||
|
|
||||||
def process_hvamp(self, rtio_offset, peripheral):
|
def process_hvamp(self, rtio_offset, peripheral):
|
||||||
hvamp_name = self.get_name("hvamp")
|
hvamp_name = self.get_name("hvamp")
|
||||||
|
|
|
@ -8,6 +8,7 @@ import sys
|
||||||
|
|
||||||
from artiq.experiment import *
|
from artiq.experiment import *
|
||||||
from artiq.coredevice.ad9910 import AD9910, SyncDataEeprom
|
from artiq.coredevice.ad9910 import AD9910, SyncDataEeprom
|
||||||
|
from artiq.coredevice.phaser import PHASER_GW_BASE, PHASER_GW_MIQRO
|
||||||
from artiq.master.databases import DeviceDB
|
from artiq.master.databases import DeviceDB
|
||||||
from artiq.master.worker_db import DeviceManager
|
from artiq.master.worker_db import DeviceManager
|
||||||
|
|
||||||
|
@ -570,20 +571,37 @@ class SinaraTester(EnvExperiment):
|
||||||
self.core.break_realtime()
|
self.core.break_realtime()
|
||||||
phaser.init()
|
phaser.init()
|
||||||
delay(1*ms)
|
delay(1*ms)
|
||||||
phaser.channel[0].set_duc_frequency(duc)
|
if phaser.gw_rev == PHASER_GW_BASE:
|
||||||
phaser.channel[0].set_duc_cfg()
|
phaser.channel[0].set_duc_frequency(duc)
|
||||||
phaser.channel[0].set_att(6*dB)
|
phaser.channel[0].set_duc_cfg()
|
||||||
phaser.channel[1].set_duc_frequency(-duc)
|
phaser.channel[0].set_att(6*dB)
|
||||||
phaser.channel[1].set_duc_cfg()
|
phaser.channel[1].set_duc_frequency(-duc)
|
||||||
phaser.channel[1].set_att(6*dB)
|
phaser.channel[1].set_duc_cfg()
|
||||||
phaser.duc_stb()
|
phaser.channel[1].set_att(6*dB)
|
||||||
delay(1*ms)
|
phaser.duc_stb()
|
||||||
for i in range(len(osc)):
|
|
||||||
phaser.channel[0].oscillator[i].set_frequency(osc[i])
|
|
||||||
phaser.channel[0].oscillator[i].set_amplitude_phase(.2)
|
|
||||||
phaser.channel[1].oscillator[i].set_frequency(-osc[i])
|
|
||||||
phaser.channel[1].oscillator[i].set_amplitude_phase(.2)
|
|
||||||
delay(1*ms)
|
delay(1*ms)
|
||||||
|
for i in range(len(osc)):
|
||||||
|
phaser.channel[0].oscillator[i].set_frequency(osc[i])
|
||||||
|
phaser.channel[0].oscillator[i].set_amplitude_phase(.2)
|
||||||
|
phaser.channel[1].oscillator[i].set_frequency(-osc[i])
|
||||||
|
phaser.channel[1].oscillator[i].set_amplitude_phase(.2)
|
||||||
|
delay(1*ms)
|
||||||
|
elif phaser.gw_rev == PHASER_GW_MIQRO:
|
||||||
|
for ch in range(2):
|
||||||
|
phaser.channel[ch].set_att(6*dB)
|
||||||
|
phaser.channel[ch].set_duc_cfg()
|
||||||
|
sign = 1. - 2.*ch
|
||||||
|
for i in range(len(osc)):
|
||||||
|
phaser.channel[ch].miqro.set_profile(i, profile=1,
|
||||||
|
frequency=sign*(duc + osc[i]), amplitude=1./len(osc))
|
||||||
|
delay(100*us)
|
||||||
|
phaser.channel[ch].miqro.set_window(
|
||||||
|
start=0x000, iq=[[1., 0.]], order=0, tail=0)
|
||||||
|
phaser.channel[ch].miqro.pulse(
|
||||||
|
window=0x000, profiles=[1 for _ in range(len(osc))])
|
||||||
|
delay(1*ms)
|
||||||
|
else:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def phaser_led_wave(self, phasers):
|
def phaser_led_wave(self, phasers):
|
||||||
|
|
|
@ -709,20 +709,33 @@ class Phaser(_EEM):
|
||||||
) for pol in "pn"]
|
) for pol in "pn"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_std(cls, target, eem, iostandard=default_iostandard):
|
def add_std(cls, target, eem, mode="base", iostandard=default_iostandard):
|
||||||
cls.add_extension(target, eem, iostandard=iostandard)
|
cls.add_extension(target, eem, iostandard=iostandard)
|
||||||
|
|
||||||
phy = phaser.Phaser(
|
if mode == "base":
|
||||||
target.platform.request("phaser{}_ser_p".format(eem)),
|
phy = phaser.Base(
|
||||||
target.platform.request("phaser{}_ser_n".format(eem)))
|
target.platform.request("phaser{}_ser_p".format(eem)),
|
||||||
target.submodules += phy
|
target.platform.request("phaser{}_ser_n".format(eem)))
|
||||||
target.rtio_channels.extend([
|
target.submodules += phy
|
||||||
rtio.Channel.from_phy(phy, ififo_depth=4),
|
target.rtio_channels.extend([
|
||||||
rtio.Channel.from_phy(phy.ch0.frequency),
|
rtio.Channel.from_phy(phy, ififo_depth=4),
|
||||||
rtio.Channel.from_phy(phy.ch0.phase_amplitude),
|
rtio.Channel.from_phy(phy.ch0.frequency),
|
||||||
rtio.Channel.from_phy(phy.ch1.frequency),
|
rtio.Channel.from_phy(phy.ch0.phase_amplitude),
|
||||||
rtio.Channel.from_phy(phy.ch1.phase_amplitude),
|
rtio.Channel.from_phy(phy.ch1.frequency),
|
||||||
])
|
rtio.Channel.from_phy(phy.ch1.phase_amplitude),
|
||||||
|
])
|
||||||
|
elif mode == "miqro":
|
||||||
|
phy = phaser.Miqro(
|
||||||
|
target.platform.request("phaser{}_ser_p".format(eem)),
|
||||||
|
target.platform.request("phaser{}_ser_n".format(eem)))
|
||||||
|
target.submodules += phy
|
||||||
|
target.rtio_channels.extend([
|
||||||
|
rtio.Channel.from_phy(phy, ififo_depth=4),
|
||||||
|
rtio.Channel.from_phy(phy.ch0),
|
||||||
|
rtio.Channel.from_phy(phy.ch1),
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
raise ValueError("invalid mode", mode)
|
||||||
|
|
||||||
|
|
||||||
class HVAmp(_EEM):
|
class HVAmp(_EEM):
|
||||||
|
|
|
@ -123,13 +123,14 @@ def peripheral_fastino(module, peripheral, **kwargs):
|
||||||
def peripheral_phaser(module, peripheral, **kwargs):
|
def peripheral_phaser(module, peripheral, **kwargs):
|
||||||
if len(peripheral["ports"]) != 1:
|
if len(peripheral["ports"]) != 1:
|
||||||
raise ValueError("wrong number of ports")
|
raise ValueError("wrong number of ports")
|
||||||
eem.Phaser.add_std(module, peripheral["ports"][0], **kwargs)
|
eem.Phaser.add_std(module, peripheral["ports"][0],
|
||||||
|
peripheral.get("mode", "base"), **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def peripheral_hvamp(module, peripheral, **kwargs):
|
def peripheral_hvamp(module, peripheral, **kwargs):
|
||||||
if len(peripheral["ports"]) != 1:
|
if len(peripheral["ports"]) != 1:
|
||||||
raise ValueError("wrong number of ports")
|
raise ValueError("wrong number of ports")
|
||||||
eem.HVAmp.add_std(module, peripheral["ports"][0],
|
eem.HVAmp.add_std(module, peripheral["ports"][0],
|
||||||
ttl_simple.Output, **kwargs)
|
ttl_simple.Output, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ class DDSChannel(Module):
|
||||||
[Cat(i.a, i.clr, i.p) for i in self.dds.i])
|
[Cat(i.a, i.clr, i.p) for i in self.dds.i])
|
||||||
|
|
||||||
|
|
||||||
class Phaser(Module):
|
class Base(Module):
|
||||||
def __init__(self, pins, pins_n):
|
def __init__(self, pins, pins_n):
|
||||||
self.rtlink = rtlink.Interface(
|
self.rtlink = rtlink.Interface(
|
||||||
rtlink.OInterface(data_width=8, address_width=8,
|
rtlink.OInterface(data_width=8, address_width=8,
|
||||||
|
@ -87,3 +87,93 @@ class Phaser(Module):
|
||||||
self.rtlink.i.stb.eq(re_dly[0] & self.serializer.stb),
|
self.rtlink.i.stb.eq(re_dly[0] & self.serializer.stb),
|
||||||
self.rtlink.i.data.eq(self.serializer.readback),
|
self.rtlink.i.data.eq(self.serializer.readback),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class MiqroChannel(Module):
|
||||||
|
def __init__(self):
|
||||||
|
self.rtlink = rtlink.Interface(
|
||||||
|
rtlink.OInterface(data_width=30, address_width=2, fine_ts_width=1,
|
||||||
|
enable_replace=False))
|
||||||
|
self.pulse = Signal(128)
|
||||||
|
self.ack = Signal()
|
||||||
|
regs = [Signal(30, reset_less=True) for _ in range(3)]
|
||||||
|
dt = Signal(7, reset_less=True)
|
||||||
|
stb = Signal()
|
||||||
|
pulse = Cat(stb, dt, regs)
|
||||||
|
assert len(self.pulse) >= len(pulse)
|
||||||
|
self.comb += [
|
||||||
|
self.pulse.eq(pulse),
|
||||||
|
self.rtlink.o.busy.eq(stb & ~self.ack),
|
||||||
|
]
|
||||||
|
self.sync.rtio += [
|
||||||
|
If(~stb,
|
||||||
|
dt.eq(dt + 2),
|
||||||
|
),
|
||||||
|
If(self.ack,
|
||||||
|
dt[1:].eq(0),
|
||||||
|
stb.eq(0),
|
||||||
|
If(stb,
|
||||||
|
[r.eq(0) for r in regs],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
If(self.rtlink.o.stb,
|
||||||
|
Array(regs)[self.rtlink.o.address].eq(self.rtlink.o.data),
|
||||||
|
If(self.rtlink.o.address == 0,
|
||||||
|
dt[0].eq(self.rtlink.o.fine_ts),
|
||||||
|
stb.eq(1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Miqro(Module):
|
||||||
|
def __init__(self, pins, pins_n):
|
||||||
|
self.rtlink = rtlink.Interface(
|
||||||
|
rtlink.OInterface(data_width=8, address_width=8,
|
||||||
|
enable_replace=False),
|
||||||
|
rtlink.IInterface(data_width=10))
|
||||||
|
|
||||||
|
self.submodules.ch0 = MiqroChannel()
|
||||||
|
self.submodules.ch1 = MiqroChannel()
|
||||||
|
|
||||||
|
self.submodules.serializer = SerDes(
|
||||||
|
n_data=8, t_clk=8, d_clk=0b00001111,
|
||||||
|
n_frame=10, n_crc=6, poly=0x2f)
|
||||||
|
self.submodules.intf = SerInterface(pins, pins_n)
|
||||||
|
self.comb += [
|
||||||
|
Cat(self.intf.data[:-1]).eq(Cat(self.serializer.data[:-1])),
|
||||||
|
self.serializer.data[-1].eq(self.intf.data[-1]),
|
||||||
|
]
|
||||||
|
|
||||||
|
header = Record([
|
||||||
|
("we", 1),
|
||||||
|
("addr", 7),
|
||||||
|
("data", 8),
|
||||||
|
("type", 4)
|
||||||
|
])
|
||||||
|
self.comb += [
|
||||||
|
self.serializer.payload.eq(Cat(
|
||||||
|
header.raw_bits(),
|
||||||
|
self.ch0.pulse,
|
||||||
|
self.ch1.pulse,
|
||||||
|
)),
|
||||||
|
self.ch0.ack.eq(self.serializer.stb),
|
||||||
|
self.ch1.ack.eq(self.serializer.stb),
|
||||||
|
]
|
||||||
|
|
||||||
|
re_dly = Signal(3) # stage, send, respond
|
||||||
|
self.sync.rtio += [
|
||||||
|
header.type.eq(3), # body type is miqro pulse data
|
||||||
|
If(self.serializer.stb,
|
||||||
|
header.we.eq(0),
|
||||||
|
re_dly.eq(re_dly[1:]),
|
||||||
|
),
|
||||||
|
If(self.rtlink.o.stb,
|
||||||
|
re_dly[-1].eq(~self.rtlink.o.address[-1]),
|
||||||
|
header.we.eq(self.rtlink.o.address[-1]),
|
||||||
|
header.addr.eq(self.rtlink.o.address),
|
||||||
|
header.data.eq(self.rtlink.o.data),
|
||||||
|
),
|
||||||
|
self.rtlink.i.stb.eq(re_dly[0] & self.serializer.stb),
|
||||||
|
self.rtlink.i.data.eq(self.serializer.readback),
|
||||||
|
]
|
||||||
|
|
Loading…
Reference in New Issue