The python script tdc_demo.py can be downloaded in the repository.
With this file should be put the devmem.py
This software is used to configure the ADC board but also to process the
delay between the pulses. The incoming pulses are first transformed into
damped sines. We then apply a fitting function on it to determine the
best suited mathematical model. Among the parameters of the mathematical
model is the phase. By determining the phase for all the incomming
pulses and by computing the difference between them, it is possible to
figure out what was the delay between them.
As a reminder, the board uses a Zynq device. On such SoC, we have both a
FPGA (Programmable Logic or PL) and a dual core ARM (Processing System
or PS). On this board are installed python3, the numpy and scipy
The first part of the script is used to set the gateware and reset the
ADC. First, the ADC is configured by the SPI communication. Before
taking the measurements, the ADC is reset by tuning it off and on.
Multiples parameters can be set using via this interface such as the ADC
output mode or the output phase. It is also possible to test if the
gateware is working properly by asking to the ADC to generate a bit
The reset sequence is ordered like this :
SERDES clock distribution
The ADC is controled by the GPIO0 pins 5, 6, 7 respectively the chip
select, the clock and the data pins. The script is used to configure the
ADC in DDR mode.
Two LEDs are also provided on the board. They are connected on GPIO0 pin
0 and 4. In the tdc_demo.py they are used to tell the user if the
recording has been triggered.
The AXI bus can be accessed by the Linux running on the board by
accessing slot 0x40000000 in the memory.
The first step to acquire data is to configure the data buffer and the
trigger. A pretrigger can be set for prototyping/debugging. In our case,
it has been set to 10 samples. This allows us to see if the trigger
worked correctly by plotting the waveform. If the trigger threshold
level is not correctly set, it is possible to miss the first
alternations of our signal, this may generate errors.
Once the triggers are set and armed, we are able to catch the data. One
of the LEDs should be turned on to inform that the setup is done and the
system is waiting for data to come. For the moment, the code is poling
the triggers register in order to detect when data has been caught.
The other LED should light up when the pulse has been detected and the
data transfer form the PL to the PS can be done. Once the PS has read
the buffers, the fitting algorithm can be applied.
The fitting algorithm is done on 110 pts. This number of points has been
experimentally determined as optimal. The 15 firsts points of the frame
are not used. On one side, this is because of the pre trigger. The 10
firsts points are just used for debugging. The 5 following points are
removed to "clean the signal", more on that later.
The fitting is done by a scipy built-in function:
scipy.optimize.curve_fit(f, xdata, ydata) assuming ydata = f(xdata, *params) + eps. As we use a damped sine shape signal, the used
f : the signal frequency. This is estimated in comparison with the
sampling frequency (FS). This constant has to be set at the
beginning of the script. Any error on this frequency will generate
errors on the measurements.
phi : the phase shift. This phase shift is here returned as a
tau : the damping factor of our function.
offset : as our signal is not perfectly centered on zero, the
offset parameters has to be computed.
The parameter phi returns the phase shift in picosecond. If this
fitting function is applied to the signal coming on different channels,
we can deduce their independent phase shift. The difference between
these phase shifts can be computed to determine the delay between the
Correction of the incoming wave form
The five first points are removed from the fitting algorithm. This is
due to the fact that the signal shapes at the beginning is distorted
(figure 1). This distortion is due to the CMOS gate slew rate (see
filter-section for more explanations). The best way to
avoid those distortion to add errors on the fitting algorithm is to
remove them before applying the
Figure 1 - Distortion at the beginning of the pulse
Correction of the fitting algorithm
It is possible that the signal is interpreted the wrong way by the
algorithm. For example, it is possible that the mathematical model that
fits the signal has a negative amplitude. The phase delay is then
determined with a multiple of the signal frequency of error. It is
possible to correct it by analysing both the phase and the amplitude
sign on a case by case basis. The actual solution does not work in all
the cases and a deeper analysis is required to improve it.
This works for delays smaller than the sampling period (10 ns). It is
possible to determine bigger delays but some steps have to be added to
the algorithm and maybe some gateware modification would be required.
For bigger delays, the trigger on the different channels has to be
independent and the time between them has to be counted. This can be
done by implementing a counter in the ADC core.
The mathematical model can be improved in order to increase the
precision on the measurements. The Improvements wiki
page indicates a direction for the futur researches.
This class is used to handle the GPIOs.
__init__(self, mem, addr): The builder. Offset address is 0x0000.
direction(self, pin, is_out): Set the pin direction (0 for in and
1 for out).
set(self, pin, value): Set or clear an output value on one pin.
get(self, pin): Return boolean input pin value.
Used to control the SPI communication trough the GPIOs.
__init__(self, gpio, cs_pin, sck_pin, data_pin): The builder. All
the pins are defined here.
cs(self, value): Set the value on the chip select pin.
sck(self, value): Set the value on the clock pin.
set_sdata(self, value): Used to send one data bit on the data pin.
get_sdata(self): Used to read one data bit on the data pin.
txrx(self, data, n_bits): Transmits or reads n_bits. If data is
set to zero, this function just reads what is on the data line.
Controls the hardware trigger inside the FPGA.
__init__(self, mem, addr): The builder. The different addresses
are : 0x1000 for channel 0, 0x3000 for channel 1, 0x5000 for channel
2 and 0x7000 for channel 3.
configure(self, edge, threshold_lo, threshold_hi, mask): Set the
trigger independently. Set the triggering edge (Rising edge if 0 and
Falling edge if 1). The two threshold levels are set here. Finally,
the mask is used to link the trigger to the other channels. If the
trigger mask is set to 0xF, this means that any event on any channel
will trigger the record on all the channels. Any other combination
of trigger can be configured.
The table 1 shows an example of configuration. Each corresponds to a
trigger mask. On this example, with its mask set to 0xF, an event on
trigger0 will trigger the recording on all the channels. Trigger1, with
a 0x6 mask, will trigger the recording on channel 2 and 1. An event on
channel 2 will not trigger any recording as the mask is 0x0. And
finally, an event occurring on channel 3 will only trigger the recording
on its channel.
force(self): Used to force the trigger. Mainly used for debugging.
triggered(self): Return True if an eligible event occurred on this
arm(self): Arm the trigger
This cyclical buffer is used to record the data. It is continuously
recording the data. Once the pointer arrives at the end of the buffer,
it starts recording since the very beginning. If an event is detected by
a trigger, the pointer continues to write data but stops after looping
avoiding erasing data arrived after the event. By stopping the pointer
earlier, it is possible to save pretrigger samples.
__init__(self, mem, addr): The builder. The possible addresses are
: 0x2000 for channel 0, 0x4000 for channel 1, 0x6000 for channel 2
and 0x8000 for channel 3.
set_pretrigger(self, pretrigger): Number of samples kept before
start(self): Launch the continuous recording in the buffer.
ready(self): Returns True when the buffer data is ready to be
transferred to the Ps.
read(self): Returns the formatted data out of the buffer. This
needs the sign_extend(value, bits) global function used to format
This class is set for the ADC. It is basically represented by its SPI
__init__(self, spi): The builder. It needs the SPI interface to be
write_reg(self, reg, value): It is used to write in the ADC
read_reg(self, ref): Used to read a specific ADC register.