-
Notifications
You must be signed in to change notification settings - Fork 0
/
miqro.py
344 lines (299 loc) · 14.8 KB
/
miqro.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
from numpy import int32
from artiq import *
# 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
class Phaser:
"""Phaser mock"""
def __init__(self, channel_base=0):
self.channel_base = channel_base
self.channel0 = Channel(self, 0)
self.t_frame = seconds_to_mu(10*8*4*ns)
def write16(self, addr, data):
assert addr == PHASER_ADDR_MIQRO_MEM_ADDR
self.write32(addr, data)
def write32(self, addr, data):
rtio_output((self.channel_base << 8) | (addr & 0x7f) | 0x80, data)
delay_mu(self.t_frame)
class Channel:
"""Phaser Channel mock"""
def __init__(self, phaser, index):
self.phaser = phaser
if index != 0:
raise NotImplementedError()
self.index = index
self.miqro = Miqro(self)
# :class:`Miqro` is straigth from artiq/coredevice/phaser.py
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_profiles = 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::
To refer to an operation as "expensive" does not mean it is impossible,
merely 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])