forked from M-Labs/artiq
1
0
Fork 0

Merge pull request #1962 from quartiq/miqro

Support MIQRO mode for Phaser
This commit is contained in:
Robert Jördens 2022-10-19 16:56:02 +02:00 committed by GitHub
commit e5c621751f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 548 additions and 63 deletions

View File

@ -9,7 +9,8 @@ Unreleased
Highlights:
* Implemented Phaser-servo. This requires recent gateware on Phaser.
* Implemented Phaser-MIQRO support. This requires the Phaser MIQRO gateware
variant.
ARTIQ-7
-------

View File

@ -9,6 +9,10 @@ from artiq.coredevice.trf372017 import TRF372017
PHASER_BOARD_ID = 19
PHASER_GW_BASE = 1
PHASER_GW_MIQRO = 2
PHASER_ADDR_BOARD_ID = 0x00
PHASER_ADDR_HW_REV = 0x01
PHASER_ADDR_GW_REV = 0x02
@ -47,6 +51,12 @@ PHASER_ADDR_SERVO_CFG1 = 0x31
# 0x32 - 0x71 servo coefficients + offset data
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_TRF0 = 1 << 1
@ -78,6 +88,26 @@ class Phaser:
Phaser contains a 4 channel, 1 GS/s DAC chip with integrated upconversion,
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
MS/s and 14 bit per quadrature. Each data stream supports 5 independent
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
(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.
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
FPGA registers.
**Servo**
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
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.tune_fifo_offset = tune_fifo_offset
self.sync_dly = sync_dly
self.gw_rev = -1 # discovered in init()
self.dac_mmap = DAC34H84(dac).get_mmap()
@ -226,9 +267,9 @@ class Phaser:
delay(.1*ms) # slack
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:
print("gw_rev:", gw_rev)
print("gw_rev:", self.gw_rev)
self.core.break_realtime()
delay(.1*ms) # slack
@ -346,10 +387,11 @@ class Phaser:
if channel.get_att_mu() != 0x5a:
raise ValueError("attenuator test failed")
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)
if self.gw_rev == PHASER_GW_BASE:
# test oscillators and DUC
for i in range(len(channel.oscillator)):
oscillator = channel.oscillator[i]
@ -377,6 +419,9 @@ class Phaser:
abs(data_i - data_q) > 2):
raise ValueError("DUC+oscillator phase/amplitude test failed")
if self.gw_rev == PHASER_GW_MIQRO:
channel.miqro.reset()
if is_baseband:
continue
@ -782,6 +827,8 @@ class Phaser:
if good & (1 << o):
sum += o
count += 1
if count == 0:
raise ValueError("no good fifo offset")
best = ((sum // count) + offset) % 8
self.dac_write(0x09, (config9 & 0x1fff) | (best << 13))
return best
@ -792,8 +839,9 @@ class PhaserChannel:
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,
* a :class:`Miqro` instance on Phaser,
* several channel-specific settings in the DAC:
* quadrature modulation compensation QMC
@ -805,6 +853,7 @@ class PhaserChannel:
Attributes:
* :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
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
or overflow after the interpolation. Either band-limit any changes
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"}
@ -826,6 +877,7 @@ class PhaserChannel:
self.trf_mmap = TRF372017(trf).get_mmap()
self.oscillator = [PhaserOscillator(self, osc) for osc in range(5)]
self.miqro = Miqro(self)
@kernel
def get_dac_data(self) -> TInt32:
@ -1149,8 +1201,8 @@ class PhaserChannel:
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
filters, the ``x_offset`` (setpoint) resolution depends on the selected gains.
Low ``ki`` gains will lead to a low ``x_offset`` resolution.
filters, the ``x_offset`` (setpoint) resolution depends on the selected
gains. Low ``ki`` gains will lead to a low ``x_offset`` resolution.
The transfer function is (up to time discretization and
coefficient quantization errors):
@ -1269,3 +1321,305 @@ class PhaserOscillator:
raise ValueError("amplitude out of bounds")
pow = int32(round(phase*(1 << 16)))
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])

View File

@ -559,6 +559,13 @@ class PeripheralManager:
return 1
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("""
device_db["{name}"] = {{
"type": "local",
@ -566,12 +573,13 @@ class PeripheralManager:
"class": "Phaser",
"arguments": {{
"channel_base": 0x{channel:06x},
"miso_delay": 1,
"miso_delay": 1{dac}
}}
}}""",
name=self.get_name("phaser"),
dac=dac,
channel=rtio_offset)
return 5
return n_channels
def process_hvamp(self, rtio_offset, peripheral):
hvamp_name = self.get_name("hvamp")

View File

@ -8,6 +8,7 @@ import sys
from artiq.experiment import *
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.worker_db import DeviceManager
@ -570,6 +571,7 @@ class SinaraTester(EnvExperiment):
self.core.break_realtime()
phaser.init()
delay(1*ms)
if phaser.gw_rev == PHASER_GW_BASE:
phaser.channel[0].set_duc_frequency(duc)
phaser.channel[0].set_duc_cfg()
phaser.channel[0].set_att(6*dB)
@ -584,6 +586,22 @@ class SinaraTester(EnvExperiment):
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
def phaser_led_wave(self, phasers):

View File

@ -709,10 +709,11 @@ class Phaser(_EEM):
) for pol in "pn"]
@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)
phy = phaser.Phaser(
if mode == "base":
phy = phaser.Base(
target.platform.request("phaser{}_ser_p".format(eem)),
target.platform.request("phaser{}_ser_n".format(eem)))
target.submodules += phy
@ -723,6 +724,18 @@ class Phaser(_EEM):
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):

View File

@ -123,7 +123,8 @@ def peripheral_fastino(module, peripheral, **kwargs):
def peripheral_phaser(module, peripheral, **kwargs):
if len(peripheral["ports"]) != 1:
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):

View File

@ -27,7 +27,7 @@ class DDSChannel(Module):
[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):
self.rtlink = rtlink.Interface(
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.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),
]