For use with: Student’s laptop
and Alpaca kernel
Complete this notebook during the practical session.
16C: Oscillator - Light Sensor#
Learning goals:
I&I4: Build a Controlled Relaxation Oscillator and explore its signals
Include a diode in the feedback line, and add a variable current source
I&I5: Build an Illumination Station for Light Sensing
Replace the variable current source by a photodiode and add a current-controlled LED
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.
The Feedback line consisting of:
a Diode
D1anda Resistor
Rc
An external current source consisting of:
In the Implement 4 section:
a (variable) DC voltage source and a resistor
Rext = 100k
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:
The Potmeter
J13to implement a variableRcThe
-12Vsource of your Alpaca via the resistorRext = 100kCria’s Voltmeter for visualising Oscillator’s switching
Only in the SCR: Use the Oscilloscope to visualise the signal with more insight.
Measure the output of the integrator
VoutOA1onCH1and the output of the comparatorVoutonCH2You 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:
The
-12Vsource of your Alpaca Use the PotmeterJ13to implement a variableVcontrolfrom-12V to 0V
⚠️ Warning
Remember to turn off the Alpaca’s +12V and -12V switches for safety during the (dis-)assembly.
Connect-12VtoJ13:leftandGNDtoJ13:right
The variableVcontroloutput is available fromJ13:centerpin.
The resistor
Rext = 100kThe resistor
Rc = 10kCria’s Voltmeter for visualising Oscillator’s switching
Only in the SCR: Use the Oscilloscope to visualise the signal with more insight.
Measure the output of the integrator
VoutOA1onCH1and the output of the comparatorVoutonCH2You 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:
Mini bread board aka Minibox
OPAMP of your choice
LED (preferably white LED)
In the worst case scenario, use one of the Alpaca’s onboard LEDs
Resistor
Rled = 1kAlpaca’s Potmeter
J13and+5VsourceJ13:leftto any+5Vpin
J13:centerto the OPAMP input
J13:righttoGNDPhotodiode
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.Optional: Piece of the drink pipe or an equivalent element to focus the LED on the photodiode
💡 AttentionIt 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
Combine two or three wires to extend their length and make
afemale-malewire out ofmale-maleandfemale-femalewires.Remember to check the orientation/polarity of the photodiode with respect to GND and the inverting input of the integrator
The pins of the OPAMP for reference:
ℹ️ 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.
Decide which signal is most suitable for the analog to digital conversion and measure it with
ADC0.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#
Set the LED brightness with Potmeter
Run the measurement
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:
Discuss the motivation for using the relaxation oscillator.
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")