For use with: Student’s laptop
and
Alpaca kernel

Complete this notebook during the practical session.

16C: Oscillator - Light Sensor#

Learning goals:

  1. I&I4: Build a Controlled Relaxation Oscillator and explore its signals

    • Include a diode in the feedback line, and add a variable current source

  2. I&I5: Build an Illumination Station for Light Sensing

    • Replace the variable current source by a photodiode and add a current-controlled LED

  3. I&I6: Understand how the Light Sensor works and how to operate it

    • Use the given functions, record the signal, extract the frequency, and calculate the measured light intensity

Timing of this experiment:

  • 10+40+20+30+20 = 120 min

This is a challenging assignment. Please support your partner in solving problems.

IMPLEMENT & INVESTIGATE - Part B: Light Sensor#

⏳ Estimated total time: 120 min

Recap: Barebone Oscillator#

⏳ Estimated time: 10 min

Start by making sure that your Barebone Oscillator is correct, and that you can confidently navigate this minimal layout and can identify the essential nodes in your circuit before proceeding with the implementation.

ℹ️ Detailed schematic of the suggested Barebone Oscillator

Implement & Investigate 4:
Controlling the Relaxation Oscillator#

⏳ Estimated time: 60 min

In this assignment, you will upgrade your Basic Relaxation Oscillator into a Controlled Relaxation Oscillator by introducing two additional essential components.

  1. The Feedback line consisting of:

    • a Diode D1 and

    • a Resistor Rc

  2. An external current source consisting of:

    1. In the Implement 4 section:

      • a (variable) DC voltage source and a resistor Rext = 100k

    2. In the Implement 5 section onwards:

      • a Photodiode


Relaxation Oscillator with an Input - Circuit Schematic


To better understand how each of these components characterise the oscillator, you will explore two configurations to independently control the charging and the discharging current.

Controlling the Discharge Current#

⏳ Estimated time: 40 min

Implement the circuit in the schematic below by adding the necessary components to your Barebone Oscillator. Vary the discharge current and write down your observations. Pay attention to the frequency, amplitude, and the symmetry of the oscillation.


Relaxation Oscillator with a Variable Discharge Current


Example: Discharge Current Adjustment Procedure#

Use the following components:

  1. The Potmeter J13 to implement a variable Rc

  2. The -12V source of your Alpaca via the resistor Rext = 100k

  3. Cria’s Voltmeter for visualising Oscillator’s switching

  4. Only in the SCR: Use the Oscilloscope to visualise the signal with more insight.

    • Measure the output of the integrator VoutOA1 on CH1 and the output of the comparator Vout on CH2

      • You can keep the Oscilloscope probes attached and the measurement running for the next part of this assignment.

ℹ️ Detailed schematic for adjusting the discharging current



## Controlling the Discharge Current - Notes

Controlling the Charging Current#

⏳ Estimated time: 20 min

Implement the circuit in the schematic below. Control the charging current and write down your observations. Similarily to the previous test, pay attention to the frequency, amplitude, and the symmetry of the oscillation.


Relaxation Oscillator with a Variable Charging Current


Example: Charging Current Adjustment Procedure#

Use the following components:

  1. The -12V source of your Alpaca Use the Potmeter J13 to implement a variable Vcontrol from -12V to 0V
    ⚠️ Warning

    Remember to turn off the Alpaca’s +12V and -12V switches for safety during the (dis-)assembly.

    Connect -12V to J13:left and GNDto J13:right
    The variable Vcontrol output is available from J13:center pin.

  1. The resistor Rext = 100k

  2. The resistor Rc = 10k

  3. Cria’s Voltmeter for visualising Oscillator’s switching

  4. Only in the SCR: Use the Oscilloscope to visualise the signal with more insight.

    • Measure the output of the integrator VoutOA1 on CH1 and the output of the comparator Vout on CH2

    • You can keep the Oscilloscope probes attached and the measurement running for the remaining part of this assignment.

ℹ️ Detailed schematic for adjusting the charging current



# Controlling the Charging Current - Notes

Implement & Investigate 5: The Illumination Station#

⏳ Estimated time: 30 min

Introduce a Photodiode utilised in the photoconductive mode as an external current source to control the Oscillator with light. By adding a current-controlled LED Station, you will be able to test your circuit as a light sensor. Have a look into the Background section for a refresher on a current controller circuit for a LED, and use the figure below to get an overview of your implementation.


Implementation blueprint: Relaxation Oscillator + Illumination Station

Build the Illumination station using the small bread board (Minibox) to maintain a better overview with the dense wiring. Equivalently, you could use the upper part of Alpaca’s Sandbox if you prefer, or if you are missing the Minibox.

Building the Illumination Station#


Illumination Station

List of components:

  1. Mini bread board aka Minibox

  2. OPAMP of your choice

  3. LED (preferably white LED)

    • In the worst case scenario, use one of the Alpaca’s onboard LEDs

  4. Resistor Rled = 1k

  5. Alpaca’s Potmeter J13 and +5V source

    J13:left to any +5V pin
    J13:center to the OPAMP input
    J13:right to GND

  6. Photodiode

  7. A few wires

    You will need many of them today, but some suggested connections can be optimised and some wires reused for new connections, so go ahead and optimise the connections, and own your setup.

    Be careful when organizing your elements in the Sandbox and in the Minibox such that they don’t make a short circuit by accident.

  8. Optional: Piece of the drink pipe or an equivalent element to focus the LED on the photodiode
    💡 Attention

    It is essential for ensuring reproducibility of your measurements and the calibration that the LED is fixed with respect to the photodiode and pointing directly at it, so it is advised to place those two elements next to each other in the same bread board.


ℹ️ Hints
  1. Combine two or three wires to extend their length and make
    a female-male wire out of male-male and female-female wires.

  2. Remember to check the orientation/polarity of the photodiode with respect to GND and the inverting input of the integrator

  3. The pins of the OPAMP for reference:

4. Bear in mind that the choice of the LED and the color of the LED can influence your measurements.
ℹ️ Detailed schematic for building the Illumination Station



Controlling the Oscillator with Light#

Turn the Potmeter and observe how adjusting light intensity changes the oscillator’s frequency measured at VoutOA1 with Cria’s Voltmeter (and in the SCR - with the Oscilloscope too)

How does this adjustment compare to the charging/discharging current adjustments in the previous section?

Write down your (qualitative) observations about:

  • the response of the LED brightness across the Potmeter range

  • the frequency response of the oscillator to the LED brightness adjustment

## Controlling the Oscillator with Light - Notes

Implement & Investigate 6: Light Sensor#

⏳ Estimated time: 20 min

At this point, you are only two steps away from turning your oscillator into a light sensor.

  1. Decide which signal is most suitable for the analog to digital conversion and measure it with ADC0.

  2. Choose functions from the list below to perform the measurement and extract the light intensity.

ℹ️ Detailed schematic for measuring the light intensity


Initializing required libraries and defining custom functions#

## Initialize the connection with your Alpaca
%serialconnect to --port="COM4" 
## Import libraries
import time
import numpy as np
import matplotlib.pyplot as plt
from machine import ADC
from functiongenerator import FuncGen, DC
## Instantiate a measurement pin Ain0 and Ain1 for the input signals
adc0 = ADC(26) 
adc1 = ADC(27) 
## Measurement functions

#############################################
#############################################
# Default initial values
DELAY_MS = 2
NUM_SAMPLES = 2048

#############################################
# Function to make a simple measurement with ADC0 
def MeasureADC0(DELAY_MS=DELAY_MS, NUM_SAMPLES=NUM_SAMPLES):

    acquired_signal = np.zeros(NUM_SAMPLES)
    
    for ii in range(NUM_SAMPLES):
        # Take a sample every DELAY_MS
        sample = adc0.read_u16()
        acquired_signal[ii] = sample
        
        time.sleep_ms(DELAY_MS)
    print('Measurement complete!')        
    return acquired_signal

###############################################
# Function to make a simple measurement with ADC1 
def MeasureADC1(DELAY_MS=DELAY_MS, NUM_SAMPLES=NUM_SAMPLES):

    acquired_signal = np.zeros(NUM_SAMPLES)
    
    for ii in range(NUM_SAMPLES):
        # Take a sample every DELAY_MS
        sample = adc1.read_u16()
        acquired_signal[ii] = sample
        
        time.sleep_ms(DELAY_MS)
    print('Measurement complete!')        
    return acquired_signal

###############################################
# Function to make a simple measurement with ADC0 and ADC1
def MeasureADC0andADC1(DELAY_MS=DELAY_MS, NUM_SAMPLES=NUM_SAMPLES):

    a0 = np.zeros(NUM_SAMPLES)
    a1 = np.zeros(NUM_SAMPLES)
    for ii in range(NUM_SAMPLES):
        # Take a sample every DELAY_MS
        sample0 = adc0.read_u16()
        sample1 = adc1.read_u16()
        a0[ii] = sample0
        a1[ii] = sample1
        
        time.sleep_ms(DELAY_MS)
    print('Measurement complete!')        
    return a0,a1

################################################
# Function to make a measurement with ADC0 and set a given DC voltage on DAC A
def SetDAC_and_MeasureADC0(dc_voltage, DELAY_MS=DELAY_MS, NUM_SAMPLES=NUM_SAMPLES):

    acquired_signal = np.zeros(NUM_SAMPLES)
    
    with FuncGen(DC(V=dc_voltage)):
    
        time.sleep_ms(100)
        
        for ii in range(NUM_SAMPLES):
            # Take a sample every DELAY_MS
            sample = adc0.read_u16()
            acquired_signal[ii] = sample
            
            time.sleep_ms(DELAY_MS)
    print('Measurement complete!')        
    return acquired_signal


################################################
# Function to make a measurement with ADC0 and ADC1, and set a given DC voltage on DAC A
def SetDAC_and_MeasureADC0andADC1(dc_voltage, DELAY_MS=DELAY_MS, NUM_SAMPLES=NUM_SAMPLES):

    a0 = np.zeros(NUM_SAMPLES)
    a1 = np.zeros(NUM_SAMPLES)
    
    with FuncGen(DC(V=dc_voltage)):
    
        time.sleep_ms(100)
            
        for ii in range(NUM_SAMPLES):
            # Take a sample every DELAY_MS
            sample0 = adc0.read_u16()
            sample1 = adc1.read_u16()
            a0[ii] = sample0
            a1[ii] = sample1
            
            time.sleep_ms(DELAY_MS)
    print('Measurement complete!')        
    return a0,a1

###############################################
# Function for plotting the acquired signal
def plot_acquired_signal(acquired_signal, gain=0.1):
    acquired_signal = acquired_signal / 65535 * 3.0
    acquired_signal /= gain
    plt.plot(acquired_signal)
    plt.plot([0],[0]) # Lazy trick to get the X-axis in the plot
    plt.ylabel('Output [V]')
    plt.xlabel('Sample number')
## FFT - related functions
#############################################
#############################################

#############################################
# Function to calculate the nearest power of 2
def nearest_power_of_2(num):
    return 2 ** int(np.ceil(np.log2(num)))

#############################################
# Function to calculate the dominant friequency in the acquired signal
def calculate_meas_freq(acquired_signal, DELAY_MS=DELAY_MS, NUM_SAMPLES=NUM_SAMPLES):
    fft_output = np.fft.fft(acquired_signal)
    sampling_rate = 1 / DELAY_MS * 1000 

    re = fft_output[0]
    im = fft_output[1]
    amp2 = re**2 + im**2

    DFT = amp2[0:NUM_SAMPLES//2]
    DFT[1:] = amp2[-1:(NUM_SAMPLES+1)//2:-1]
    DFT     /= NUM_SAMPLES
    DFT[0] = 0

    N = len(acquired_signal) // 2  # Adjust for the unique format
    freq = [i * 0.5 *sampling_rate / N for i in range(N)]

    max_index = np.argmax(DFT)
    meas_freq = freq[max_index]
    
    return meas_freq

##############################################
# Function to calculate and plot the frequency spectrum of the acquired signal
def plot_meas_freq(acquired_signal, DELAY_MS=DELAY_MS, NUM_SAMPLES=NUM_SAMPLES):
    fft_output = np.fft.fft(acquired_signal)
    sampling_rate = 1 / DELAY_MS * 1000 

    re = fft_output[0]
    im = fft_output[1]
    amp2 = re**2 + im**2

    DFT = amp2[0:NUM_SAMPLES//2]
    DFT[1:] = amp2[-1:(NUM_SAMPLES+1)//2:-1]
    DFT     /= NUM_SAMPLES
    DFT[0] = 0

    N = len(acquired_signal) // 2  # Adjust for the unique format
    freq = [i * 0.5 *sampling_rate / N for i in range(N)]

    max_index = np.argmax(DFT)
    meas_freq = freq[max_index]
    
    plt.plot(freq, DFT)

    return meas_freq
## Light Intensity vs Frequency relation
#############################################
#############################################


# Function to extract the light intensity from the measured signal frequency
def Light_intensity(frequency):
    L = - np.sqrt(Idiode**2 - 4 * frequency * C * dV * Idiode) + Idiode
    L /= 2 * Rlambda * Aphotodiode
    return L

Setting up the Measurement#

Declare the values of the components used in your circuit.

# ## Settings

# Define the the scales for the measurement
desired_num_samples = 2000 # a number of samples you want - it will be adapted for use with fft
DELAY_MS = 2 # 2ms is the lowest value possible

# Declare the values of the components used in your oscillator
C = ... # Capacitance in Farads, so 1e9 for 1nF
Ra = ... # Use the notation 1e3 for 1k
Rb = ...
Rc = ...

# Parameters (do not change)
Rlambda = 0.62 # A/W
Aphotodiode = 1e-6 #m^2

# Simplifications (do not change)
dV = 2*Ra/Rb*10.8  # Voltage range (Vhigh_threshold - Vlow_threshold) in Volts
Idiode = 10.8/Rc # Amps

### Pre-configure (do not change)
NUM_SAMPLES = nearest_power_of_2(desired_num_samples)  # Adjusted to nearest power of 2 for fft

Light Intensity Measurement#

  1. Set the LED brightness with Potmeter

  2. Run the measurement

  3. Extract the light intensity by following the steps suggested in the comments.

# Run the measurement on ADC0
Ain0 = ...
# Plot the first 1000 points of the acquired signal
# Plot the frequency spectrum


# Extract the dominant frequency
F = ...

# Print the summary
print('Measured dominant frequency = %.2f Hz' % F)
# Compute the light intensity 
L = ...
# Plot the result along with the response curve
Frange = np.linspace(0, F+5, 100)
Lrange = np.array([Light_intensity(Fs) for Fs in Frange])

plt.plot(Frange, Lrange, label='Sensor response curve') 
plt.plot([F],[L] , 'ro', label='Measured light intensity') # Plotting L at F as a red dot 
plt.xlabel('Frequency [Hz]') 
plt.ylabel('Light Intensity [W/m2]') 
plt.legend()    

print('Measured light intensity %.2f W/m2   [Frequency = %.2f Hz]' % (L, F))

Compare & Conclude: per group of 4#

⏳ Estimated time: 20 min

Congratulations!#

This was a long and a difficult assignment, so you probably discussed it with your peers already along the way, but the final reflection gives everyone in the group a chance to compare your results, experiences and conclusions.

  • Please help each other understand today’s results.

  • Compare your results with your other group members.

To be checked off by a TA:

  1. Discuss the motivation for using the relaxation oscillator.

  2. Explain how your light-sensing relaxation oscillator works.

    • Which voltage did you measure and why?

    • Explain how the oscillator is charged and discharged.

    • Explain why it might be necessary to calibrate your light sensor and how you could do that.

### Conclusions 

OPTIONAL

Advanced Implement & Investigate
Sensor Characterisation and Calibration#

In this optional section, you can find some ideas to learn more about the working principle of your light-sensing relaxation oscillator. If you would like to discuss your experiments and explore further, feel free to contact Maciej (m.m.topyla@tudelft.nl) for more info.

Idea 1: Sensor Characterisation#

Measure the frequency for across a range of LED brightness and plot Frequency vs. DAC voltage.

This experiment can show you qualitatively whether your sensor produces unique values in the range of light intensities of your LED, or whether a calibration is required.

ℹ️ Detailed schematic for characterising the Light Sensor

Light Sensor Characterisation





### Settings
desired_num_samples = 2000 # Original number of samples you wanted
DELAY_MS = 2

# Define constants
C = 470e-9  # Capacitance in Farads
Ra = 3.3e3
Rb = 5e3
Rc = 10e3

Rlambda = 0.4 # A/W
Aphotodiode = 1e-6 #m2

dV = 2*Ra/Rb*10.8  # Voltage range (Vhigh_threshold - Vlow_threshold) in Volts
Idiode = 10.8/Rc

### Pre-configure
NUM_SAMPLES = nearest_power_of_2(desired_num_samples)  # Adjusted to nearest power of 2
#print(NUM_SAMPLES)
# Plot Photocurrent vs Frequency relations for a set of Idiode
Iphoto = np.linspace(1e-8, 1.5e-4, 100)  # Iphoto range from 0.01 µA to 150 µA

# Range of Idiode values
Idiode_values = [1e-4, 2e-4, 1e-3,10e-3]  # Idiode from 1e-4 to 1e-2 in steps

# Plot for each Idiode value
for Idiode in Idiode_values:
    F = -Iphoto * (Iphoto - Idiode) / (C * dV * Idiode)  # Calculate frequency
    plt.plot(Iphoto * 1e6, F, label=f"Idiode = {1e3*Idiode:.} mA")

# Plot settings
plt.xlabel("Photocurrent Iphoto (µA)")
plt.ylabel("Frequency F (Hz)")
plt.title("Frequency of Oscillation vs Photocurrent for Different Idiode")
plt.ylim(0,20)
plt.grid(True)
plt.legend(title="Idiode Values")
# Test your setup: Set DAC A and Measure ADC0 
dc_voltage = 2
a0 = SetDAC_and_MeasureADC0(dc_voltage)
# Test your setup: Plot the Integrator signal acquired with ADC0
plot_acquired_signal(a0[:1000])
# Record measurements for different DC voltages from 0.0 to 2.0 V
dc_voltages = np.linspace(0.0, 2.0, 21)
meas_freqs = np.zeros(len(dc_voltages))

for ii, dc_voltage in enumerate(dc_voltages):
    a0 = SetDAC_and_MeasureADC0(dc_voltage)
    meas_freqs[ii] = calculate_meas_freq(a0)
    print('DC voltage = %.2f V, measured frequency = %.2f Hz' % (dc_voltage, meas_freqs[ii]))
    time.sleep_ms(100)
# Plot the measured frequency as a function of the DC voltage
plt.plot(dc_voltages, meas_freqs)
plt.ylabel('Measured frequency [Hz]')
plt.xlabel('DC voltage [V]')

Idea 2: Sensor Calibration#

Measure the frequency and the photocurrent simultaneously to derive the average photodiode sensitivity for the blue LED.

This experiment can help you determine whether the parameters chosen in the Settings section reflect the reality, or further measurements and calibration are required.

ℹ️ Detailed schematic for calibrating the Light Sensor

Light Sensor Calibration



### Settings
desired_num_samples = 2000 # Original number of samples you wanted
DELAY_MS = 2

# Define constants
C = 470e-9  # Capacitance in Farads
Ra = 3.3e3
Rb = 5e3
Rc = 10e3
Rd = 100e3

Rlambda = 0.4 # A/W
Aphotodiode = 1e-6 #m2

dV = 2*Ra/Rb*10.8  # Voltage range (Vhigh_threshold - Vlow_threshold) in Volts
Idiode = 10.8/Rc

### Pre-configure
NUM_SAMPLES = nearest_power_of_2(desired_num_samples)  # Adjusted to nearest power of 2
#print(NUM_SAMPLES)
# Test your setup: Set DAC A and Measure ADC0 and ADC1
dc_voltage = 2
a0,a1 = SetDAC_and_MeasureADC0andADC1(dc_voltage)
# Test your setup: Plot the Integrator signal acquired with ADC0
plot_acquired_signal(a0[:1000])

#print(np.max(a0)/ 65535 * 3.0 / ADC0gain)
# Plot the the photocurrent signal from the Current - to - Voltage converter acquired with ADC1
plt.plot(a1[:100] / 65535 * 3.0 / Rd * 1e6 )
plt.ylabel("Photocurrent Iphoto (µA)")
# Record measurements for different DC voltages from 0.0 to 3.0 V
dc_voltages = np.linspace(0.0, 3.0, 34)
meas_freqs = np.zeros(len(dc_voltages))
meas_currents = np.zeros(len(dc_voltages))

for ii, dc_voltage in enumerate(dc_voltages):
    a0, a1 = SetDAC_and_MeasureADC0andADC1(dc_voltage)
    meas_freqs[ii] = calculate_meas_freq(a0)
    meas_currents[ii] = np.mean(a1) / 65535 * 3.0 * Rd * 
    print('DC voltage = %.2f V, measured frequency = %.2f Hz, measured current = %.2f uA' % (dc_voltage, meas_freqs[ii], meas_currents[ii]))
    time.sleep_ms(100)
# Plot your results
Lrange = np.array([light_intensity(Fs) for Fs in meas_freqs])
Ip = Rlambda * Aphotodiode * Lrange

Iphoto = np.linspace(1e-8, 1.5e-4, 100)  # Iphoto range from 0.01 µA to 150 µA

F = -Iphoto * (Iphoto - Idiode) / (C * dV * Idiode)  # Calculate frequency
plt.plot(Iphoto * 1e6, F, label=f"Idiode = {1e3*Idiode:.} mA",)
    
plt.plot(meas_currents, meas_freqs, label=f"Measured Idiode = {1e3*ID1:.} mA")
# Plot settings
plt.xlabel("Photocurrent Iphoto (µA)")
plt.ylabel("Frequency F (Hz)")
plt.title("Frequency of Oscillation vs Photocurrent for Different Idiode")
plt.ylim(0,20)
#plt.xlim(100,101)
plt.grid(True)
plt.legend(title="Idiode Values")