This is a growing script to document the performance and development of the ADwin data acquisition and control system.
%% Cell type:code id: tags:
``` python
%pylabinline
```
%% Output
Populating the interactive namespace from numpy and matplotlib
%% Cell type:code id: tags:
``` python
fromnqlabimport*
importpandasaspd
importscipy.signalassig
```
%% Cell type:markdown id: tags:
## Read/Write Transfer Functions
First of all, we want to know the transfer functions of a very simple "Read 8 channels", "Send 8 channels" program running at different sampling frequencies (corresponding to a ProcessDelay of F_CPU/F_SAMPLE).
fig=plots.frequency_domain.amplitude_phase(amplitudes,phases,title='Response for various sample rates')
fig.axes[0].legend();
```
%% Output
%% Cell type:markdown id: tags:
## Read/Write with Fixed Delay
The above transfer functions were made when the DAC outputs are always written immediately after being read. However, we want to do calculations in the mean time. Thus, the time delay between read and write would change depending on the amount of calculations. It might therefore be beneficial to always write the data at the beginning of the Event, i.e. with a constant time delay:
fig=plots.frequency_domain.amplitude_phase(amplitude,phases,title='Response at 100kHz')
fig.axes[1].legend();
```
%% Output
%% Cell type:markdown id: tags:
Let us calculate that delay:
%% Cell type:code id: tags:
``` python
fsample=1e5
delta_t=phases['DAC:ADC']/360/phases.index*1e6
```
%% Cell type:code id: tags:
``` python
fig=delta_t.plot(logx=True,title='Time Delay in $\mu s$')
fig.grid()
fig.set_ylabel('$\Delta t$');
```
%% Output
%% Cell type:markdown id: tags:
So, roughly 17us, whereas we would expect something on the order of 1/100kHz, i.e. 10us:
%% Cell type:code id: tags:
``` python
1/fsample*1e6
```
%% Output
10.0
%% Cell type:markdown id: tags:
At first I `P2_DAC8` to write the values, however this first needs to send the values to the DAC module and then starts the conversion, also it cannot synchronously start conversion on both output modules. Therefore I switched to `P2_Write_DAC8` at the end of the Event cycle, which just populates the DAC modules with new values but doesn't convert yet, and then at the start of the cycle I trigger the conversion using `P2_Sync_All`. However this did not lead to a measurable improvement in the time delay.
%% Cell type:markdown id: tags:
## Second Order Sections
The most straight forward approach for digital filters is the so-called second-order section (SOS). It has a transfer function that looks like this:
Usually, it is normalised such that $a_0 = 1$. Any second-order filter (e.g. pole-zero, 2nd order lowpass, notch) can be written in this form, and e.g. `scipy.signal` has functions to calculate the necessary coefficients:
At 100kHz sample rate and with 8 filter modules, $8*5 = 40$ SOSs, 200 coefficients, this runs at about 46% CPU load (T12 processor module).
%% Cell type:markdown id: tags:
## Global Arrays and Parameters
%% Cell type:markdown id: tags:
### Control register
#### Par_1: Timer
Time the program is already active. Measured in seconds.
#### Par_2: Filter bank control register (FBCR)
The least significant byte is used to initiate a reload of filter coefficients from the global data array (i.e., the one that is written to from the control PC). If a bit is set, the filter coefficients of the respective bank are reloaded. Afterwards, the bit is automatically cleared.
<table>
<thead>
<tr>
<th> bit </th>
<th> 7 </th>
<th> 6 </th>
<th> 5 </th>
<th> 4 </th>
<th> 3 </th>
<th> 2 </th>
<th> 1 </th>
<th> 0 </th>
</tr>
</thead>
<tr>
<th> filter bank</th>
<td> 8 </td>
<td> 7 </td>
<td> 6 </td>
<td> 5 </td>
<td> 4 </td>
<td> 3 </td>
<td> 2 </td>
<td> 1 </td>
</tr>
</table>
#### Par_3: Ramp control register (RCR)
The least significant byte determines which channel should output a ramp. 0 is no output
and values from 1-15 correspond to the output with the respective number. The byte before that determines the step size at which the program writes values from the ramp array. For example for a value of 1, every index of the ramp array is used, while for a value of 2 every second value is written and therefore ramp frequency is doubled.
Selection of channel for the monitor function. Writes the input to DAC channel 9, the corresponding aux input to DAC channel 10 and the output to DAC channel 11.
#### Par_5: Active selection (active_sel)
Selection of channel for active data. Writes the input, corresponding aux input and output in the `active_data` array, which is accessible from the PC.
#### Par_6: Aux control register
**TODO: this could go into a fcr?**
The last byte determines on which channels the aux Voltage should be added after the calculation of the Second order Sections. If the value of the bit is 1 the aux Voltage gets added.
The assignement is:
<table>
<thead>
<tr>
<th> bit value <th>
<th> 128 <th>
<th> 64 <th>
<th> 32 <th>
<th> 16 <th>
<th> 8 <th>
<th> 4 <th>
<th> 2 <th>
<th> 1 <th>
</tr>
</thead>
<tr>
<td> output channel <td>
<td> 8 <td>
<td> 7 <td>
<td> 6 <td>
<td> 5 <td>
<td> 4 <td>
<td> 3 <td>
<td> 2 <td>
<td> 1 <td>
</tr>
</table>
#### Par_11..18: Filter control register (fcr_1..8)
Controls which functions of the respective filter (1..8) are enabled, e.g. bit 0 controls the input switch which enabled/disables the input into the filter.
<table>
<thead>
<tr>
<th> bit </th>
<th> 9 </th>
<th> 8 </th>
<th> 7 </th>
<th> 6 </th>
<th> 5 </th>
<th> 4 </th>
<th> 3 </th>
<th> 2 </th>
<th> 1 </th>
<th> 0 </th>
</tr>
</thead>
<tr>
<td> description </td>
<td> AUX </td>
<td> SOS 5 </td>
<td> SOS 4 </td>
<td> SOS 3 </td>
<td> SOS 2 </td>
<td> SOS 1 </td>
<td> unused </td>
<td> offset sw </td>
<td> output sw </td>
<td> input sw </td>
</tr>
</table>
%% Cell type:markdown id: tags:
### Data Arrays
#### Filter coefficients (Data_1)
Contains the filter coefficients. 5 for each SOS and 5 SOS for each filter module. The first 5 entries of each Filter correspond to the first SOS the second to the second and so on. For the calculation the values are refactored as mentioned above.
The assignment of Array indexes is the following:
<html>
<body>
<table>
<thead>
<tr>
<th> index <th>
<th> 1-25 <th>
<th> 26-50 <th>
<th> 51-75 <th>
<th> 76-100 <th>
<th> 101-126 <th>
<th> 126-150 <th>
<th> 151-175 <th>
<th> 176-200 <th>
</tr>
</thead>
<td> Filter Module <td>
<td> 1 <td>
<td> 2 <td>
<td> 3 <td>
<td> 4 <td>
<td> 5 <td>
<td> 6 <td>
<td> 7 <td>
<td> 8 <td>
<tr>
</tr>
</table>
</body>
</html>
#### Offset and gain (Data_2)
Contains information for the offset and gain of the filter modules. Odd numbers contain the gain multiplier and even the offset value. Assignment of slots for filter
<html>
<body>
<table>
<thead>
<tr>
<th> index <th>
<th> 1,2 <th>
<th> 3,4 <th>
<th> 5,6 <th>
<th> 7,8 <th>
<th> 9,10 <th>
<th> 11, 12 <th>
<th> 13,14 <th>
<th> 15,16 <th>
</tr>
</thead>
<tr>
<td> Filter number <td>
<td> 1 <td>
<td> 2 <td>
<td> 3 <td>
<td> 4 <td>
<td> 5 <td>
<td> 6 <td>
<td> 7 <td>
<td> 8 <td>
</tr>
</table>
</body>
</html>
#### active_data (Data_3)
The first entry is the input of Channel n, the second entry is the corresponding aux input and the third entry is the calculated output for Channel n.
%% Cell type:markdown id: tags:
## Calculation of digital filters with Python
The filters are designed in the contineous s-plane. To get a discrete time version usable for calculation in realtime with a set frequency, one has to transform the contineous transfer function. The time discret version of the laplace transform is the z-transform. The mapping between s and z is:
$$
z(s)=e^{sT}\,,
$$
or alternatively,
$$
s(z)= \frac{1}{T}\ln(z)\,,
$$
with $T$ being the inverse sampling rate. Since $\ln(z)$ is a continuous function and therefore not useful for the calculation with second order sections one has to expand it and stop the expansion after a certain order. One way to do this is stopping the expansion after the first order, this is the so called bilinear transform,
$$
s \rightarrow \frac{2}{T}\frac{z-1}{z+1}\,.
$$
Since the whole frequency axis gets mapped on the unit circle the transfer functions calculated and designed in the $s$ plane have to be prewarped. This is done by changing the relevant frequencies with:
$$
w'=\frac{2}{T}\tan\left(\omega\frac{T}{2}\right).
$$
The prewarping transform in the code is done after the poles and zeros were calculated and not beforehands.
$x$ is the position of the pole/zero in the frequency domain and $f_n$ the nyquist frequency.
This can lead to errors for Filter with a higher order than 1.$$ \\ $$
But for most cases there is no difference as one can see at the example of the second order lowpass and the second order notch filter. The location of the poles $p_{1,2}$ for both is:
So the result of the bilinear transform is the exact same for any Q value larger than 0.5.
%% Cell type:markdown id: tags:
## Special properties of the Adwin System
### Integer Overflow of the write DAC
Since the ADwin system works with 32 bit signed integer numbers and the DAC-module takes only unsigned 16 bit integers, an overflow is created when numbers smaller than 0 or numbers larger than 65535 are used. For values smaller than 0 the last 15 bits and the sign bit are interpreted as 16 bit integer number. For values larger than 65535 the last 16 bits are cutoff so for example. If one wants to write 65536 in the output one gets a 0 which represents -10V or for the negative value of -1 one gets an output of 3.
### Functions are macros
Care needs to be taken when specifying calculations in function arguments, as they really are only macros that get replaced inline. E.g., if one has a function
Function MultiplyBy2(x) as Integer
MultiplyBy2 = x * 2
EndFunction
and then calls this function like `MultiplyBy2(a+1)`, this gets replaced with the actual code `a+1 * 2`, which is obviously evaluated to `a + 2` instead of `(a+1) * 2`.