Fork PyRPL on GitHub

3. API manual

This manual will guide you step-by-step through the programming interface of each PyRPL modules.

3.1. 1 First steps

If the installation went well, you should now be able to load the package in python. If that works you can pass directly to the next section ‘Connecting to the RedPitaya’.

from pyrpl import Pyrpl

Sometimes, python has problems finding the path to pyrpl. In that case you should add the pyrplockbox directory to your pythonpath environment variable (http://stackoverflow.com/questions/3402168/permanently-add-a-directory-to-pythonpath). If you do not know how to do that, just manually navigate the ipython console to the directory, for example:

cd c:\lneuhaus\github\pyrpl

Now retry to load the module. It should really work now.

from pyrpl import Pyrpl

3.1.1. Connecting to the RedPitaya

You should have a working SD card (any version of the SD card content is okay) in your RedPitaya (for instructions see http://redpitaya.com/quick-start/). The RedPitaya should be connected via ethernet to your computer. To set this up, there is plenty of instructions on the RedPitaya website (http://redpitaya.com/quick-start/). If you type the ip address of your module in a browser, you should be able to start the different apps from the manufacturer. The default address is http://192.168.1.100. If this works, we can load the python interface of pyrplockbox by specifying the RedPitaya’s ip address.

HOSTNAME = "192.168.1.100"
from pyrpl import Pyrpl
p = Pyrpl(hostname=HOSTNAME)

If you see at least one ‘>’ symbol, your computer has successfully connected to your RedPitaya via SSH. This means that your connection works. The message ‘Server application started on port 2222’ means that your computer has sucessfully installed and started a server application on your RedPitaya. Once you get ‘Client started with success’, your python session has successfully connected to that server and all things are in place to get started.

3.1.2. Basic communication with your RedPitaya

# Access the RedPitaya object in charge of communicating with the board
r = p.rp

#check the value of input1
print r.scope.voltage1

With the last command, you have successfully retrieved a value from an FPGA register. This operation takes about 300 ?s on my computer. So there is enough time to repeat the reading n times.

#see how the adc reading fluctuates over time
import time
from matplotlib import pyplot as plt
times, data = [],[]
t0 = time.time()
n = 3000
for i in range(n):
    times.append(time.time()-t0)
    data.append(r.scope.voltage_in1)
print("Rough time to read one FPGA register: ", (time.time()-t0)/n*1e6, "?s")
%matplotlib inline
f, axarr = plt.subplots(1,2, sharey=True)
axarr[0].plot(times, data, "+")
axarr[0].set_title("ADC voltage vs time")
axarr[1].hist(data, bins=10,normed=True, orientation="horizontal")
axarr[1].set_title("ADC voltage histogram")

You see that the input values are not exactly zero. This is normal with all RedPitayas as some offsets are hard to keep zero when the environment changes (temperature etc.). So we will have to compensate for the offsets with our software. Another thing is that you see quite a bit of scatter beetween the points - almost as much that you do not see that the datapoints are quantized. The conclusion here is that the input noise is typically not totally negligible. Therefore we will need to use every trick at hand to get optimal noise performance.

After reading from the RedPitaya, let’s now try to write to the register controlling the first 8 yellow LED’s on the board. The number written to the LED register is displayed on the LED array in binary representation. You should see some fast flashing of the yellow leds for a few seconds when you execute the next block.

#blink some leds for 5 seconds
from time import sleep
for i in range(1025):
    r.hk.led=i
    sleep(0.005)
# now feel free to play around a little to get familiar with binary representation by looking at the leds.
from time import sleep
r.hk.led = 0b00000001
for i in range(10):
    r.hk.led = ~r.hk.led>>1
    sleep(0.2)
import random
for i in range(100):
    r.hk.led = random.randint(0,255)
    sleep(0.02)

3.2. 2 RedPitaya (or Hardware) modules

Let’s now look a bit closer at the class RedPitaya. Besides managing the communication with your board, it contains different modules that represent the different sections of the FPGA. You already encountered two of them in the example above: “hk” and “scope”. Here is the full list of modules:

r.hk #"housekeeping" = LEDs and digital inputs/outputs
r.ams #"analog mixed signals" = auxiliary ADCs and DACs.

r.scope #oscilloscope interface

r.asg0 #"arbitrary signal generator" channel 0
r.asg1 #"arbitrary signal generator" channel 1

r.pid0 #first of three PID modules
r.pid1
r.pid2

r.iq0 #first of three I+Q quadrature demodulation/modulation modules
r.iq1
r.iq2

r.iir #"infinite impulse response" filter module that can realize complex transfer functions

3.2.1. ASG and Scope module

3.2.2. Arbitrary Signal Generator

There are two Arbitrary Signal Generator modules: asg1 and asg2. For these modules, any waveform composed of \(2^{14}\) programmable points is sent to the output with arbitrary frequency and start phase upon a trigger event.

Let’s set up the ASG to output a sawtooth signal of amplitude 0.8 V (peak-to-peak 1.6 V) at 1 MHz on output 2:

asg.output_direct = 'out2'
asg.setup(waveform='halframp', frequency=20e4, amplitude=0.8, offset=0, trigger_source='immediately')

3.2.3. Oscilloscope

The scope works similar to the ASG but in reverse: Two channels are available. A table of \(2^{14}\) datapoints for each channel is filled with the time series of incoming data. Downloading a full trace takes about 10 ms over standard ethernet. The rate at which the memory is filled is the sampling rate (125 MHz) divided by the value of ‘decimation’. The property ‘average’ decides whether each datapoint is a single sample or the average of all samples over the decimation interval.

s = r.scope # shortcut
print("Available decimation factors:", s.decimations)
print("Trigger sources:", s.trigger_sources)
print("Available inputs: ", s.inputs)

Let’s have a look at a signal generated by asg1. Later we will use convenience functions to reduce the amount of code necessary to set up the scope:

asg = r.asg1
s = r.scope

# turn off asg so the scope has a chance to measure its "off-state" as well
asg.output_direct = "off"

# setup scope
s.input1 = 'asg1'

# pass asg signal through pid0 with a simple integrator - just for fun (detailed explanations for pid will follow)
r.pid0.input = 'asg1'
r.pid0.ival = 0 # reset the integrator to zero
r.pid0.i = 1000 # unity gain frequency of 1000 hz
r.pid0.p = 1.0 # proportional gain of 1.0
r.pid0.inputfilter = [0,0,0,0] # leave input filter disabled for now

# show pid output on channel2
s.input2 = 'pid0'

# trig at zero volt crossing
s.threshold_ch1 = 0

# positive/negative slope is detected by waiting for input to
# sweep through hysteresis around the trigger threshold in
# the right direction
s.hysteresis_ch1 = 0.01

# trigger on the input signal positive slope
s.trigger_source = 'ch1_positive_edge'

# take data symetrically around the trigger event
s.trigger_delay = 0

# set decimation factor to 64 -> full scope trace is 8ns * 2^14 * decimation = 8.3 ms long
s.decimation = 64

# launch a single (asynchronous) curve acquisition, the asynchronous
# acquisition means that the function returns immediately, eventhough the
# data-acquisition is still going on.
res = s.curve_async()

print("Before turning on asg:")
print("Curve ready:", s.curve_ready()) # trigger should still be armed

# turn on asg and leave enough time for the scope to record the data
asg.setup(frequency=1e3, amplitude=0.3, start_phase=90, waveform='halframp', trigger_source='immediately')
sleep(0.010)

# check that the trigger has been disarmed
print("After turning on asg:")
print("Curve ready:", s.curve_ready())
print("Trigger event age [ms]:",8e-9*((
s.current_timestamp&0xFFFFFFFFFFFFFFFF) - s.trigger_timestamp)*1000)

# The function curve_async returns a *future* (or promise) of the curve. To
# access the actual curve, use result()
ch1, ch2 = res.result()

# plot the data
%matplotlib inline
plt.plot(s.times*1e3, ch1, s.times*1e3, ch2)
plt.xlabel("Time [ms]")
plt.ylabel("Voltage")

What do we see? The blue trace for channel 1 shows just the output signal of the asg. The time=0 corresponds to the trigger event. One can see that the trigger was not activated by the constant signal of 0 at the beginning, since it did not cross the hysteresis interval. One can also see a ‘bug’: After setting up the asg, it outputs the first value of its data table until its waveform output is triggered. For the halframp signal, as it is implemented in pyrpl, this is the maximally negative value. However, we passed the argument start_phase=90 to the asg.setup function, which shifts the first point by a quarter period. Can you guess what happens when we set start_phase=180? You should try it out!

In green, we see the same signal, filtered through the pid module. The nonzero proportional gain leads to instant jumps along with the asg signal. The integrator is responsible for the constant decrease rate at the beginning, and the low-pass that smoothens the asg waveform a little. One can also foresee that, if we are not paying attention, too large an integrator gain will quickly saturate the outputs.

# useful functions for scope diagnostics
print("Curve ready:", s.curve_ready())
print("Trigger source:",s.trigger_source)
print("Trigger threshold [V]:",s.threshold_ch1)
print("Averaging:",s.average)
print("Trigger delay [s]:",s.trigger_delay)
print("Trace duration [s]: ",s.duration)
print("Trigger hysteresis [V]", s.hysteresis_ch1)
print("Current scope time [cycles]:",hex(s.current_timestamp))
print("Trigger time [cycles]:",hex(s.trigger_timestamp))
print("Current voltage on channel 1 [V]:", r.scope.voltage_in1)
print("First point in data buffer 1 [V]:", s.ch1_firstpoint)

3.2.4. PID module

We have already seen some use of the pid module above. There are three PID modules available: pid0 to pid2.

print r.pid0.help()

3.2.4.1. Proportional and integral gain

#make shortcut
pid = r.pid0

#turn off by setting gains to zero
pid.p,pid.i = 0,0
print("P/I gain when turned off:", pid.i,pid.p)
# small nonzero numbers set gain to minimum value - avoids rounding off to zero gain
pid.p = 1e-100
pid.i = 1e-100
print("Minimum proportional gain: ", pid.p)
print("Minimum integral unity-gain frequency [Hz]: ", pid.i)
# saturation at maximum values
pid.p = 1e100
pid.i = 1e100
print("Maximum proportional gain: ", pid.p)
print("Maximum integral unity-gain frequency [Hz]: ", pid.i)

3.2.4.2. Control with the integral value register

import numpy as np
#make shortcut
pid = r.pid0

# set input to asg1
pid.input = "asg1"

# set asg to constant 0.1 Volts
r.asg1.setup(waveform="dc", offset = 0.1)

# set scope ch1 to pid0
r.scope.input1 = 'pid0'

#turn off the gains for now
pid.p,pid.i = 0, 0

#set integral value to zero
pid.ival = 0

#prepare data recording
from time import time
times, ivals, outputs = [], [], []

# turn on integrator to whatever negative gain
pid.i = -10

# set integral value above the maximum positive voltage
pid.ival = 1.5

#take 1000 points - jitter of the ethernet delay will add a noise here but we dont care
for n in range(1000):
    times.append(time())
    ivals.append(pid.ival)
    outputs.append(r.scope.voltage_in1)

#plot
import matplotlib.pyplot as plt
%matplotlib inline
times = np.array(times)-min(times)
plt.plot(times,ivals,times,outputs)
plt.xlabel("Time [s]")
plt.ylabel("Voltage")

Again, what do we see? We set up the pid module with a constant (positive) input from the ASG. We then turned on the integrator (with negative gain), which will inevitably lead to a slow drift of the output towards negative voltages (blue trace). We had set the integral value above the positive saturation voltage, such that it takes longer until it reaches the negative saturation voltage. The output of the pid module is bound to saturate at +- 1 Volts, which is clearly visible in the green trace. The value of the integral is internally represented by a 32 bit number, so it can practically take arbitrarily large values compared to the 14 bit output. You can set it within the range from +4 to -4V, for example if you want to exloit the delay, or even if you want to compensate it with proportional gain.

3.2.4.3. Input filters

The pid module has one more feature: A bank of 4 input filters in series. These filters can be either off (bandwidth=0), lowpass (bandwidth positive) or highpass (bandwidth negative). The way these filters were implemented demands that the filter bandwidths can only take values that scale as the powers of 2.

# off by default
r.pid0.inputfilter
# minimum cutoff frequency is 1.1 Hz, maximum 3.1 MHz (for now)
r.pid0.inputfilter = [1,1e10,-1,-1e10]
print(r.pid0.inputfilter)
# not setting a coefficient turns that filter off
r.pid0.inputfilter = [0,4,8]
print(r.pid0.inputfilter)
# setting without list also works
r.pid0.inputfilter = -2000
print(r.pid0.inputfilter)
# turn off again
r.pid0.inputfilter = []
print(r.pid0.inputfilter)

You should now go back to the Scope and ASG example above and play around with the setting of these filters to convince yourself that they do what they are supposed to.

3.2.5. IQ module

Demodulation of a signal means convolving it with a sine and cosine at the ‘carrier frequency’. The two resulting signals are usually low-pass filtered and called ‘quadrature I’ and ‘quadrature Q’. Based on this simple idea, the IQ module of pyrpl can implement several functionalities, depending on the particular setting of the various registers. In most cases, the configuration can be completely carried out through the setup function of the module.

Lock-in detection / PDH / synchronous detection

#reload to make sure settings are default ones
from pyrpl import Pyrpl
r = Pyrpl(hostname="192.168.1.100").rp

#shortcut
iq = r.iq0

# modulation/demodulation frequency 25 MHz
# two lowpass filters with 10 and 20 kHz bandwidth
# input signal is analog input 1
# input AC-coupled with cutoff frequency near 50 kHz
# modulation amplitude 0.1 V
# modulation goes to out1
# output_signal is the demodulated quadrature 1
# quadrature_1 is amplified by 10
iq.setup(frequency=25e6, bandwidth=[10e3,20e3], gain=0.0,
         phase=0, acbandwidth=50000, amplitude=0.5,
         input='in1', output_direct='out1',
         output_signal='quadrature', quadrature_factor=10)

After this setup, the demodulated quadrature is available as the output_signal of iq0, and can serve for example as the input of a PID module to stabilize the frequency of a laser to a reference cavity. The module was tested and is in daily use in our lab. Frequencies as low as 20 Hz and as high as 50 MHz have been used for this technique. At the present time, the functionality of a PDH-like detection as the one set up above cannot be conveniently tested internally. We plan to upgrade the IQ-module to VCO functionality in the near future, which will also enable testing the PDH functionality.

3.2.5.1. Network analyzer

When implementing complex functionality in the RedPitaya, the network analyzer module is by far the most useful tool for diagnostics. The network analyzer is able to probe the transfer function of any other module or external device by exciting the device with a sine of variable frequency and analyzing the resulting output from that device. This is done by demodulating the device output (=network analyzer input) with the same sine that was used for the excitation and a corresponding cosine, lowpass-filtering, and averaging the two quadratures for a well-defined number of cycles. From the two quadratures, one can extract the magnitude and phase shift of the device’s transfer function at the probed frequencies. Let’s illustrate the behaviour. For this example, you should connect output 1 to input 1 of your RedPitaya, such that we can compare the analog transfer function to a reference. Make sure you put a 50 Ohm terminator in parallel with input 1.

# shortcut for na
na = p.networkanalyzer
na.iq_name = 'iq1'

# setup network analyzer with the right parameters
na.setup(start=1e3,stop=62.5e6,points=1001,rbw=1000, avg=1,
amplitude=0.2,input='iq1',output_direct='off', acbandwidth=0)

#take transfer functions. first: iq1 -> iq1, second iq1->out1->(your cable)->adc1
iq1 = na.curve()
na.setup(input='in1', output_direct='out1')
in1 = na.curve()

# get x-axis for plotting
f = na.frequencies

#plot
from pyrpl.hardware_modules.iir.iir_theory import bodeplot
%matplotlib inline
bodeplot([(f, iq1, "iq1->iq1"), (f, in1, "iq1->out1->in1->iq1")], xlog=True)

If your cable is properly connected, you will see that both magnitudes are near 0 dB over most of the frequency range. Near the Nyquist frequency (62.5 MHz), one can see that the internal signal remains flat while the analog signal is strongly attenuated, as it should be to avoid aliasing. One can also see that the delay (phase lag) of the internal signal is much less than the one through the analog signal path.

Note

The Network Analyzer is implemented as a software module, distinct from the iq module. This is the reason why networkanalyzer is accessed directly at the Pyrpl-object level p.networkanalyzer and not at the redpitaya level p.rp.networkanalyzer. However, an iq module is reserved whenever the network analyzer is acquiring data.

If you have executed the last example (PDH detection) in this python session, iq0 should still send a modulation to out1, which is added to the signal of the network analyzer, and sampled by input1. In this case, you should see a little peak near the PDH modulation frequency, which was 25 MHz in the example above.

3.2.5.2. Lorentzian bandpass filter

The iq module can also be used as a bandpass filter with continuously tunable phase. Let’s measure the transfer function of such a bandpass with the network analyzer:

# shortcut for na and bpf (bandpass filter)
na = p.networkanalyzer
bpf = p.rp.iq2

# setup bandpass
bpf.setup(frequency = 2.5e6, #center frequency
          bandwidth=1.e3, # the filter quality factor
          acbandwidth = 10e5, # ac filter to remove pot. input offsets
          phase=0, # nominal phase at center frequency (propagation phase lags not accounted for)
          gain=2.0, # peak gain = +6 dB
          output_direct='off',
          output_signal='output_direct',
          input='iq1')

# setup the network analyzer
na.setup(start=1e5, stop=4e6, points=201, rbw=100, avg=3,
                         amplitude=0.2, input='iq2',output_direct='off')

# take transfer function
tf1 = na.curve()

# add a phase advance of 82.3 degrees and measure transfer function
bpf.phase = 82.3
tf2 = na.curve()

f = na.frequencies

#plot
from pyrpl.hardware_modules.iir.iir_theory import bodeplot
%matplotlib inline
bodeplot([(f, tf1, "phase = 0.0"), (f, tf2, "phase = %.1f"%bpf.phase)])

Note

To measure the transfer function of an internal module, we cannot

use the output_direct property of the network ananlyzer (only ‘out1’, ‘out2’ or ‘off’ are allowed). To circumvent the problem, we set the input of the module to be measured to the network analyzer’s iq.

3.2.5.3. Frequency comparator module

To lock the frequency of a VCO (Voltage controlled oscillator) to a frequency reference defined by the RedPitaya, the IQ module contains the frequency comparator block. This is how you set it up. You have to feed the output of this module through a PID block to send it to the analog output. As you will see, if your feedback is not already enabled when you turn on the module, its integrator will rapidly saturate (-585 is the maximum value here, while a value of the order of 1e-3 indicates a reasonable frequency lock).

iq = p.rp.iq0

# turn off pfd module for settings
iq.pfd_on = False

# local oscillator frequency
iq.frequency = 33.7e6

# local oscillator phase
iq.phase = 0
iq.input = 'in1'
iq.output_direct = 'off'
iq.output_signal = 'pfd'

print("Before turning on:")
print("Frequency difference error integral", iq.pfd_integral)

print("After turning on:")
iq.pfd_on = True
for i in range(10):
    print("Frequency difference error integral", iq.pfd_integral)

3.2.6. IIR module

Sometimes it is interesting to realize even more complicated filters. This is the case, for example, when a piezo resonance limits the maximum gain of a feedback loop. For these situations, the IIR module can implement filters with ‘Infinite Impulse Response’ (https://en.wikipedia.org/wiki/Infinite_impulse_response). It is the your task to choose the filter to be implemented by specifying the complex values of the poles and zeros of the filter. In the current version of pyrpl, the IIR module can implement IIR filters with the following properties:

  • strictly proper transfer function (number of poles > number of zeros)
  • poles (zeros) either real or complex-conjugate pairs
  • no three or more identical real poles (zeros)
  • no two or more identical pairs of complex conjugate poles (zeros)
  • pole and zero frequencies should be larger than :math:`

rac{f_ m{nyquist}}{1000}` (but you can optimize the nyquist frequency of your filter by tuning the ‘loops’ parameter) - the DC-gain of the filter must be 1.0. Despite the FPGA implemention being more flexible, we found this constraint rather practical. If you need different behavior, pass the IIR signal through a PID module and use its input filter and proportional gain. If you still need different behaviour, the file iir.py is a good starting point. - total filter order <= 16 (realizable with 8 parallel biquads) - a remaining bug limits the dynamic range to about 30 dB before internal saturation interferes with filter performance

Filters whose poles have a positive real part are unstable by design. Zeros with positive real part lead to non-minimum phase lag. Nevertheless, the IIR module will let you implement these filters.

In general the IIR module is still fragile in the sense that you should verify the correct implementation of each filter you design. Usually you can trust the simulated transfer function. It is nevertheless a good idea to use the internal network analyzer module to actually measure the IIR transfer function with an amplitude comparable to the signal you expect to go through the filter, as to verify that no saturation of internal filter signals limits its performance.

#reload to make sure settings are default ones
from pyrpl import Pyrpl
p = Pyrpl(hostname="192.168.1.100")

#shortcut
iir = p.rp.iir

#print docstring of the setup function
print(iir.setup.__doc__)
#prepare plot parameters
%matplotlib inline
import matplotlib
matplotlib.rcParams['figure.figsize'] = (10, 6)

#setup a complicated transfer function
zeros = [ 4e4j+300, +2e5j+1000, +2e6j+3000]
poles = [ 1e6, +5e4j+300, 1e5j+3000, 1e6j+30000]
iir.setup(zeros=zeros, poles=poles, loops=None, plot=True)
print("Filter sampling frequency: ", 125./iir.loops,"MHz")

If you try changing a few coefficients, you will see that your design filter is not always properly realized. The bottleneck here is the conversion from the analytical expression (poles and zeros) to the filter coefficients, not the FPGA performance. This conversion is (among other things) limited by floating point precision. We hope to provide a more robust algorithm in future versions. If you can obtain filter coefficients by another, preferrably analytical method, this might lead to better results than our generic algorithm.

Let’s check if the filter is really working as it is supposed:

# first thing to check if the filter is not ok
print("IIR overflows before:", bool(iir.overflow))
na = p.networkanalyzer

# measure tf of iir filter
iir.input = na.iq
na.setup(iq_name='iq1', start=1e4, stop=3e6, points = 301, rbw=100, avg=1,
         amplitude=0.1, input='iir', output_direct='off', logscale=True)
tf = na.curve()

# first thing to check if the filter is not ok
print("IIR overflows after:", bool(iir.overflow))

# retrieve designed transfer function
designdata = iir.transfer_function(na.frequencies)


#plot with design data
%matplotlib inline
import matplotlib
matplotlib.rcParams['figure.figsize'] = (10, 6)
from pyrpl.hardware_modules.iir.iir_theory import bodeplot
bodeplot([(na.frequencies, designdata, "designed system"),
(na.frequencies, tf, "measured system")], xlog=True)

As you can see, the filter has trouble to realize large dynamic ranges. With the current standard design software, it takes some ‘practice’ to design transfer functions which are properly implemented by the code. While most zeros are properly realized by the filter, you see that the first two poles suffer from some kind of saturation. We are working on an automatic rescaling of the coefficients to allow for optimum dynamic range. From the overflow register printed above the plot, you can also see that the network analyzer scan caused an internal overflow in the filter. All these are signs that different parameters should be tried.

A straightforward way to impove filter performance is to adjust the DC-gain and compensate it later with the gain of a subsequent PID module. See for yourself what the parameter g=0.1 (instead of the default value g=1.0) does here:

#rescale the filter by 20 fold reduction of DC gain
iir.setup(zeros=zeros, poles=poles, g=0.1, loops=None, plot=False)

# first thing to check if the filter is not ok
print("IIR overflows before:", bool(iir.overflow))

# measure tf of iir filter
iir.input = na.iq
tf = na.curve()

# first thing to check if the filter is not ok
print("IIR overflows after:", bool(iir.overflow))

# retrieve designed transfer function
designdata = iir.transfer_function(na.frequencies)


#plot with design data
%matplotlib inline
import matplotlib
matplotlib.rcParams['figure.figsize'] = (10, 6)
from pyrpl.hardware_modules.iir.iir_theory import bodeplot
bodeplot([(na.frequencies, designdata, "designed system"),
(na.frequencies, tf, "measured system")], xlog=True)

You see that we have improved the second peak (and avoided internal overflows) at the cost of increased noise in other regions. Of course this noise can be reduced by increasing the NA averaging time. But maybe it will be detrimental to your application? After all, IIR filter design is far from trivial, but this tutorial should have given you enough information to get started and maybe to improve the way we have implemented the filter in pyrpl (e.g. by implementing automated filter coefficient scaling).

If you plan to play more with the filter, these are the remaining internal iir registers:

iir = p.rp.iir

# useful diagnostic functions
print("IIR on:", iir.on)
print("IIR bypassed:", iir.shortcut)
print("IIR copydata:", iir.copydata)
print("IIR loops:", iir.loops)
print("IIR overflows:", bin(iir.overflow))
print("Coefficients (6 per biquad):")
print(iir.coefficients)

# set the unity transfer function to the filter
iir._setup_unity()

3.3. 3 Pyrpl (or Software) modules

Software modules are modules that don’t have an FPGA counterpart. They are directly accessible at the root pyrpl object (no need to go through the redpitaya object). We have already encountered a software module above. Remember how we accessed the network analyzer module:

HOSTNAME = "192.168.1.100"
from pyrpl import Pyrpl
p = Pyrpl(hostname=HOSTNAME)

# hardware modules are members of the redpitaya object
p.rp.iq0

# software modules are members of the root pyrpl object
p.networkanalyzer

Software modules usually perform higher-level tasks than hardware modules. Moreover, accessing a hardware module without care could be harmful to some acquisition already running on the redpitaya. For this reason, it is advisable to access hardware modules via module managers only.

3.3.1. Using Module Managers

Module managers are lightweight software modules that manage the access to hardware modules. For example, to use the scope:

HOSTNAME = "192.168.1.100"
from pyrpl import Pyrpl
p = Pyrpl(hostname=HOSTNAME)

# directly accessing the scope will not *reserve* it
scope = p.rp.scope
print(scope.owner)
scope.duration = 1.

# using the scope manager changes its ownership
with p.scopes.pop('username') as scope:
   print(scope.owner)
   scope.duration =0.01
   print(scope.duration)
# The scope is freed (and reset to its previous state) after the with
# construct
print(scope.owner)
print(scope.duration)

In case several identical modules are available on the FPGA, the first one ( starting from the end of the list) is returned by the module manager:

HOSTNAME = "192.168.1.100"
from pyrpl import Pyrpl
p = Pyrpl(hostname=HOSTNAME)

# directly accessing the scope will not *reserve* it
pid2 = p.rp.pid2
pid2.owner = 'foo'

# Pid manager returns the first free pid module (in decreasing order)
with p.pids.pop('username') as pid:
   print("pid0's owner: ", p.rp.pid0.owner)
   print("pid1's owner: ", p.rp.pid1.owner)
   print("pid2's owner: ", p.rp.pid2.owner)
print("pid0's owner: ", p.rp.pid0.owner)
print("pid1's owner: ", p.rp.pid1.owner)
print("pid2's owner: ", p.rp.pid2.owner)

3.3.2. Spectrum Analyzer

The spectrum analyzer measures the magnitude of an input signal versus frequency. There are two working modes for the spectrum analyzer implemented in pyrpl:

  • iq mode: the input signal is demodulated around the center_frequency of the analysis window (using iq2). The slowly varying quadratures are subsequently sent to the 2 channels of the scope. The complex IQ time trace is built from the sum I(t) + iQ(t). The spectrum is then evaluated by performing a Fourier transforom of the the complex iq signal.
  • baseband mode: up to 2 channels are available in baseband mode. The channels are digitized by the scope and the real traces are directly Fourier transformed. Since both channels are acquired simultaneously, it is also possible to retrieve the cross spectrum between channel 1 and channel 2 (the relative phase of the fourier transform coefficients is meaningful)

At the moment, the iq mode is deactivated since we haven’t yet implemented the sharp antialiasing filters required to avoid polluting the analysis windows from aliased noise originating from outside the Nyquist frequency of the scope acquisition. However, we are planning on implementing such a filter with the iir module in the near future.

In the following example, we are going to demonstrate how to measure a sinusoidal signal and a white noise originating from an asg

# let's use a module manager for the asg
with p.asgs.pop('user') as asg:
    # setup a sine at 100 kHz
    asg.setup(frequency=1e5, waveform='sin', trigger_source='immediately', amplitude=1., offset=0)

    # setup the spectrumanalyzer in baseband mode
    p.spectrumanalyzer.setup(input1_baseband=asg, #note that input1_baseband!=input)
                             baseband=True, # only mod eavailable right now
                             span=1e6, # span of the analysis (/2 in iq mode)
                             window=blackman # filter window)

    # the return format is (spectrum for channel 1, spectrum for channel 2,
    # real part of cross spectrum, imaginary part of cross spectrum):
    ch1, ch2, cross_re, cross_im = p.spectrumanalyzer.curve()

# plot the spectrum
import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(p.spectrumanalyzer.frequencies, ch1)

We notice that the spectrum is peaked around 100 kHz (The width of the peak is given by the residual bandwidth), and the height of the peak is 1.

The internal unit of the spectrum analyzer is V_pk^2, such that a 1 V sine results in a 1 Vpk^2 peak in the spectrum. To convert the spectrum in units of noise spectral density, a utility function is provided: data_to_unit()

# let's use a module manager for the asg
with p.asgs.pop('user') as asg:
    # setup a white noise of variance 0.1 V
    asg.setup(frequency=1e5, waveform='noise', trigger_source='immediately', amplitude=0.1, offset=0)

    # setup the spectrumanalyzer in baseband mode and full span
    p.spectrumanalyzer.setup(input1_baseband=asg, baseband=True, span=125e6)

    # the return format is (spectrum for channel 1, spectrum for channel 2,
    # real part of cross spectrum, imaginary part of cross spectrum):
    ch1, ch2, cross_re, cross_im = p.spectrumanalyzer.curve()

# convert to Vrms^2/Hz
data = p.spectrumanalyzer.data_to_unit(ch1, 'Vrms^2/Hz', p.spectrumanalyzer.rbw)

# plot the spectrum
import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(p.spectrumanalyzer.frequencies, data)

# integrate spectrum from 0 to nyquist frequency
df = p.spectrumanalyzer.frequencies[1] - p.spectrumanalyzer.frequencies[0]
print(sum(data)*df)

As expected, the integral of the noise spectrum over the whole frequency range gives the variance of the noise. To know more about spectrum analysis in Pyrpl, and in particular, how the filtering windows are normalized, please refer to the section How a spectrum is computed in PyRPL.

3.3.3. Lockbox

Coming soon