<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 B2b example notebook
Example notebook for the B2b, 2 channel 24-bit ADC module. The module contains the same ADCs as the D4 and is identical in hardware to the D4b module
Step1:  Open the SPI rack connection and unlock the controller. This is necessary after bootup of the controller module. If not unlocked, no communication with the modules can take place. The virtual COM port baud rate is irrelevant as it doesn't change the actual speed. Timeout can be changed, but 1 second is a good value.
Step2:  Read back the version of the microcontroller software. This should return 1.6 or higher to be able to use the B2b properly. Als read the temperature and the battery voltages through the C1b, this way we verify that the connection with the SPI Rack is working.
Step3:  Create a new B2b module object at the correct module address using the SPI object. If we set calibrate=True, the module will run a calibration routine at initialisation. This takes about 2 seconds, during which the python code will stall all operations.
To see that we have a connection, we read back the firmware version.
Step4:  FFT
One useful application of the B2b module is to find interference. The module can be set to run at a high sample rate and store a trace in the local memory. If we run an FFT on this data, we will be able to see all kinds of interference signals present in our setup. To demonstrate this, we will use the measurement setup as shown in the image below.
<img src="Images/Meas_Setup_B2b_FFT.png" alt="Scope Image"  title="Scope Image" width="350" />
We will set the function generator to generate a 500 Hz signal of &pm;100mV and run it through a sample simulator at 10M&#8486;. The B2b will be connected to a current measurement module, in this case an old M1 I-measure module that was lying around.
First we have to configure the B2b for acquiring long traces of data.
Configuring the B2b for FFT
The B2b module can run from either a local (inside the module) clock or a user provided clock from the backplane. This backplane clock should be 10 MHz and either a square or a sine wave. If there are more modules with microcontrollers in the rack, and they need to run synchronously, it is recommended to use the backplane clock. For a single module it is fine to run it using the local clock.
If the external clock is selected but not present, the user will get an ERROR to the logger and the microcontroller will keep running on the internal clock. Never turn off the external clock if the microcontroller is running on it. This will stop the module from functioning.
In this example we will use the internal clock
Step5:  To get the B2b module to do anything, it needs to be triggered. There are three ways of triggering the module
Step6:  We'll measure on channel one (zero in software), so we need to enable it. For the FFT we'll take 10000 measurements with filter setting 0 on the sinc5 filter. This will give a datarate of 50 kSPS and a resolution of 16.8 bit. For details on all the filter settings, see the excel sheet for the D4_filter.
Step7:  Measurement and plotting
To start a measurement we trigger the B2b via software and keep checking if the module is done measuring.
Step8:  We use the periodogram from scipy, which will give the power spectral density. Before we do that we have to take the gain of the M1f module into account. It has a gain of 10 MV/A and a postgain of 10.
Step9:  D5a sweep
We're now gonna perform a sweep with the D5a and measure synchronously with the B2b module. As the timing of the D5a updates is handled by the PC, the time between the sweep steps is not going to be very accurate. This same issue would arise if we used the software trigger of the B2b. To work around this, we will trigger the B2b using the controller trigger.
The measurement setup is displayed in the figure below.
<img src="Images/Meas_Setup_B2b_D5a.png" alt="Scope Image"  title="Scope Image" width="350" />
Create a new D5a module object at the correct module address using the SPI object. By default the module resets the output voltages to 0 Volt. Before it does this, it will read back the current value. If this value is non-zero it will slowly ramp it to zero. If reset_voltages = False then the output will not be changed.
Step10:  To get nice equidistant voltage steps, we will use integer multiples of the smallest step the DAC can do in the current range setting.
Step11:  We now have to tell the B2b module to look out for the controller trigger, with an amount equal to the sweep length. Additionally we will also set a holdoff time of 1ms. This to compensate for any delays through the circuit (due to line length and/or filters).
Step12:  We will keep the filter at sinc5, but the rate at 10
Step13:  Here we see how we can synchronise the updating of the DAC with the triggering of the B2b module. Before we set the net output voltage, we arm the spi_rack controller. This means that it will send a trigger on the next SPI command it receives
Step14:  Compensating for the gain of the M1 (a factor 10e6), we get the IV curve for our 'sample'. In this case the sample simulator was set to a series resistance of 10 MOhm with all capacitors at minimum value.
Step15:  When done with this example, it is recommended to close the SPI Rack connection. This will allow other measurement scripts to access the device.
<ASSISTANT_TASK:>
Python Code:
from spirack import SPI_rack, B2b_module, D5a_module, D4b_module
import logging
from time import sleep
from tqdm import tqdm_notebook
import numpy as np
from scipy import signal
from plotly.offline import init_notebook_mode, iplot, plot
import plotly.graph_objs as go
init_notebook_mode(connected=True)
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
Explanation: B2b example notebook
Example notebook for the B2b, 2 channel 24-bit ADC module. The module contains the same ADCs as the D4 and is identical in hardware to the D4b module: the only difference being the absence of connectors on the front of the module. Both of them differ to the D4 by the addition of an ARM microcontroller. This allows for operations where exact timing and local storage is needed.
SPI Rack setup
To use the D5b module, we need to import both the D5b_module and the SPI_rack module from the spirack library. All the communication with the SPI Rack runs through the SPI_rack object which communicates through a virtual COM port. This COM port can only be open on one instance on the PC. Make sure you close the connection here before you can use it somewhere else.
We also import the logging library to be able to display the logging messages; numpy for data manipulation; scipy for the FFT analysis and plotly for visualistation.
End of explanation
COM_port = 'COM4' # COM port of the SPI rack
COM_speed = 1e6   # Baud rate, not of much importance
timeout = 1       # Timeout value in seconds
spi_rack = SPI_rack(COM_port, COM_speed, timeout)
spi_rack.unlock() # Unlock the controller to be able to send data to the rack
Explanation: Open the SPI rack connection and unlock the controller. This is necessary after bootup of the controller module. If not unlocked, no communication with the modules can take place. The virtual COM port baud rate is irrelevant as it doesn't change the actual speed. Timeout can be changed, but 1 second is a good value.
End of explanation
print('Version: ' + spi_rack.get_firmware_version())
print('Temperature: {:.2f} C'.format(spi_rack.get_temperature()))
battery_v = spi_rack.get_battery()
print('Battery: {:.3f}V, {:.3f}V'.format(battery_v[0], battery_v[1]))
Explanation: Read back the version of the microcontroller software. This should return 1.6 or higher to be able to use the B2b properly. Als read the temperature and the battery voltages through the C1b, this way we verify that the connection with the SPI Rack is working.
End of explanation
B2b = B2b_module(spi_rack, module=4, calibrate=False)
print("Firmware version: {}".format(B2b.get_firmware_version()))
Explanation: Create a new B2b module object at the correct module address using the SPI object. If we set calibrate=True, the module will run a calibration routine at initialisation. This takes about 2 seconds, during which the python code will stall all operations.
To see that we have a connection, we read back the firmware version.
End of explanation
B2b.set_clock_source('internal')
print("Clock source: {}".format(B2b.get_clock_source()))
Explanation: FFT
One useful application of the B2b module is to find interference. The module can be set to run at a high sample rate and store a trace in the local memory. If we run an FFT on this data, we will be able to see all kinds of interference signals present in our setup. To demonstrate this, we will use the measurement setup as shown in the image below.
<img src="Images/Meas_Setup_B2b_FFT.png" alt="Scope Image"  title="Scope Image" width="350" />
We will set the function generator to generate a 500 Hz signal of &pm;100mV and run it through a sample simulator at 10M&#8486;. The B2b will be connected to a current measurement module, in this case an old M1 I-measure module that was lying around.
First we have to configure the B2b for acquiring long traces of data.
Configuring the B2b for FFT
The B2b module can run from either a local (inside the module) clock or a user provided clock from the backplane. This backplane clock should be 10 MHz and either a square or a sine wave. If there are more modules with microcontrollers in the rack, and they need to run synchronously, it is recommended to use the backplane clock. For a single module it is fine to run it using the local clock.
If the external clock is selected but not present, the user will get an ERROR to the logger and the microcontroller will keep running on the internal clock. Never turn off the external clock if the microcontroller is running on it. This will stop the module from functioning.
In this example we will use the internal clock:
End of explanation
B2b.set_trigger_input("None")
B2b.set_trigger_amount(1)
B2b.set_trigger_holdoff_time(0)
Explanation: To get the B2b module to do anything, it needs to be triggered. There are three ways of triggering the module:
Software trigger
Controller generated trigger
D5b generated trigger
The software trigger is generated by the PC, which means that the timing is not very exact. Depending on the user application, this might be acceptable. As an example, it would be perfectly fine for finding interference: take a long trace and run an FFT  on the data.
The controller generated trigger eliminates the issue of the software trigger: the timing is now handled by the microcontroller in the controller module. This allows for exact alignment with other operations. There are two ways the controller can generate a trigger: directly by a PC command, or synchronous with another SPI command. This last one is the most interesting, you can for example generate a trigger at the moment you're sending a message to update the voltage on the D5a module. This allows for synchronous measurements and takes the PC out of the picture. The controller generated triggers will be on the backplane for all modules to see, so it allows the user to trigger multiple modules at once.
Finally there is also the D5b generated trigger: it generates a trigger everytime it toggles the output (in toggling mode). This allows for lock-in type of measurements. For more information on that, see the lock-in example notebook.
In this notebook we will be using both the software trigger and the controller generated trigger. First we'll use the software trigger. To do this, we'll set the trigger input to 'None' to make the B2b ignore the trigger lines on the backplane. We only expect one trigger, and we don't need any hold off time. This is a dead time which the B2b will wait after the trigger before it starts measuring. It can be set with a resolution of 100 ns.
End of explanation
filter_type = 'sinc5'
filter_setting = 0
B2b.set_ADC_enable(0, True)
B2b.set_sample_amount(0, 10000)
B2b.set_filter_type(0, filter_type)
B2b.set_filter_rate(0, filter_setting)
Explanation: We'll measure on channel one (zero in software), so we need to enable it. For the FFT we'll take 10000 measurements with filter setting 0 on the sinc5 filter. This will give a datarate of 50 kSPS and a resolution of 16.8 bit. For details on all the filter settings, see the excel sheet for the D4_filter.
End of explanation
B2b.software_trigger()
while B2b.is_running():
    sleep(0.1)
    
ADC_data, _ = B2b.get_data()
Explanation: Measurement and plotting
To start a measurement we trigger the B2b via software and keep checking if the module is done measuring.
End of explanation
#Calculate periodogram
T = B2b.sample_time[filter_type][filter_setting]
fs = 1/T
N = len(ADC_data)
gain = 10*10e6
f0, Pxx_den0 = signal.periodogram(ADC_data/gain, fs)
#Plot the FFT data
pldata0 = go.Scattergl(x=f0, y=np.sqrt(Pxx_den0), mode='lines+markers', name='ADC1')
plot_data = [pldata0]
layout = go.Layout(
    title = dict(text='Spectral Density'),
    xaxis = dict(title=r'$\text{Frequency [Hz]}$', type='log'),
    yaxis = dict(title=r'$\text{PSD [} \text{A/}\sqrt{\text{Hz}} \text{]}  $')
)
fig = go.Figure(data=plot_data, layout=layout)
iplot(fig)
Explanation: We use the periodogram from scipy, which will give the power spectral density. Before we do that we have to take the gain of the M1f module into account. It has a gain of 10 MV/A and a postgain of 10.
End of explanation
D5a = D5a_module(spi_rack, module=2, reset_voltages=True)
Explanation: D5a sweep
We're now gonna perform a sweep with the D5a and measure synchronously with the B2b module. As the timing of the D5a updates is handled by the PC, the time between the sweep steps is not going to be very accurate. This same issue would arise if we used the software trigger of the B2b. To work around this, we will trigger the B2b using the controller trigger.
The measurement setup is displayed in the figure below.
<img src="Images/Meas_Setup_B2b_D5a.png" alt="Scope Image"  title="Scope Image" width="350" />
Create a new D5a module object at the correct module address using the SPI object. By default the module resets the output voltages to 0 Volt. Before it does this, it will read back the current value. If this value is non-zero it will slowly ramp it to zero. If reset_voltages = False then the output will not be changed.
End of explanation
smallest_step = D5a.get_stepsize(0)
sweep_voltages = np.arange(-3000*smallest_step, 3001*smallest_step, 100*smallest_step)
print('Smallest step: {0:.3f} uV'.format(smallest_step*1e6))
print('Start voltage: {0:.4f} V. Stop voltage: {0:.4f} V'.format(sweep_voltages[0], sweep_voltages[-1]))
print('Sweep length: {} steps'.format(len(sweep_voltages)))
Explanation: To get nice equidistant voltage steps, we will use integer multiples of the smallest step the DAC can do in the current range setting.
End of explanation
B2b.set_trigger_input("Controller")
B2b.set_trigger_amount(len(sweep_voltages))
B2b.set_trigger_holdoff_time(10e-3)
Explanation: We now have to tell the B2b module to look out for the controller trigger, with an amount equal to the sweep length. Additionally we will also set a holdoff time of 1ms. This to compensate for any delays through the circuit (due to line length and/or filters).
End of explanation
filter_type = 'sinc5'
filter_setting = 10
B2b.set_ADC_enable(0, True)
B2b.set_sample_amount(0, 1)
B2b.set_filter_type(0, filter_type)
B2b.set_filter_rate(0, filter_setting)
Explanation: We will keep the filter at sinc5, but the rate at 10: a data rate of 1ksps with a settling time of 1 ms. This gives us a resolution of 20.7 bits. The sample_amount is now set to one: only one sample is taken per trigger.
End of explanation
for value in tqdm_notebook(sweep_voltages):
    spi_rack.trigger_arm()
    D5a.set_voltage(0, value)
    while B2b.is_running():
        sleep(1e-3)
ADC_data_sweep, _ = B2b.get_data()
Explanation: Here we see how we can synchronise the updating of the DAC with the triggering of the B2b module. Before we set the net output voltage, we arm the spi_rack controller. This means that it will send a trigger on the next SPI command it receives: in this case the D5a set_voltage command. We'll then wait a little bit to make sure measurement and settling is done, and go on to the next step.
End of explanation
gain = 10e6
pldata = go.Scattergl(x=sweep_voltages, y=ADC_data_sweep/gain, mode='lines+markers', name='ADC_data')
plot_data = [pldata]
layout = go.Layout(
    title = dict(text='10 MOhm IV Curve'),
    xaxis = dict(title='D5a voltage (V)'),
    yaxis = dict(title='Current (A)')
)
fig = go.Figure(data=plot_data, layout=layout)
iplot(fig)
Explanation: Compensating for the gain of the M1 (a factor 10e6), we get the IV curve for our 'sample'. In this case the sample simulator was set to a series resistance of 10 MOhm with all capacitors at minimum value.
End of explanation
spi_rack.close()
Explanation: When done with this example, it is recommended to close the SPI Rack connection. This will allow other measurement scripts to access the device.
End of explanation
<END_TASK>