I've just bought the following photodiode (here) and I am trying to make a simple Arduino circuit/code so that the photodiode is able to record an LED blinking at an arbitrary refresh rate (frequency).
I have an Arduino Uno driving the sensor, and a Raspberry Pi driving the LED at a given frequency.
I would like some help as my knowledge in signal processing is minimal, and I would appreciate any feedback or tips on whether what I am doing is correct or not.
Here is the Arduino code:
CODE UPDATED!!
//#include <TimerOne.h>
#include <Arduino.h>
const unsigned long ONESEC = 1000000; // microseconds
const int size = 600;
int sensorValue[size];
int dly = 1000000 / size; // delay so to fill N samples during 1 complete second (1,000,000 microseconds)
int maxValue = 0;
int minValue = 1000000;
double Alpha = 0.1; // [0.0 - 1.0] weight parameter
double filteredValue = 0.0;
double prevValue = 0.0;
double avgsum = 0.0;
int total = 0;
int avgtotal = 0;
const int s = 10;
int rounds = size / s;
int tmparr[s];
const int btnPin = 7;
const int sensorPin = A0; // sensor pin
void setup() {
Serial.begin(57600);
// init tmparr
for(int i=0;i<s;i++){
tmparr[i] = 0;
}
}
void waitForButton(int btnPin){
Serial.println("Waiting...");
while(digitalRead(btnPin) != 1){
continue;
}
Serial.println("Button has been pressed!");
}
void loop() { // code that loops forever
int i = 0;
unsigned long curTime = micros();
unsigned long prevTime, startTime;
prevTime = curTime;
startTime = curTime;
while(curTime - startTime <= ONESEC){ // sofar the time between the current and the start time is less than 1 second, keep recording data
if(curTime - prevTime < dly){ // sofar the current and the previous time stamp has a differene of less than N seconds, do not record anything
curTime = micros();
continue;
}
sensorValue[i++] = analogRead(sensorPin);
if(i > size){
break;
}
prevTime = curTime;
curTime = micros();
}
///////////////////////
///////////////////////
// Data processing ////
///////////////////////
///////////////////////
// filter the signal and find maximum/minimum and avg
for(int i=0;i<size;i++){
int v = sensorValue[i];
filteredValue = (v * Alpha) + (prevValue * (1-Alpha)); // exponential filtering
sensorValue[i] = filteredValue; // re-writing the sensor data !! RAW DATA ARE BEING OVERWRITTEN !!
avgsum = avgsum + filteredValue;
prevValue = filteredValue;
if(maxValue < filteredValue){
maxValue = filteredValue;
}
if(minValue > filteredValue){
minValue = filteredValue;
}
}
avgsum = avgsum / size;
// plotting and segregating the signal
int peaks = 0;
int prevState = -1;
for(int i=0;i<rounds;i++){
for(int j=0;j<s;j++){
int v = sensorValue[(i*s)+j];
total = total - tmparr[j];
total = total + v;
avgtotal = total / s;
tmparr[j] = v;
Serial.print(avgtotal); // new filtered value
Serial.print(",");
Serial.print(avgsum);
Serial.print(",");
Serial.println(v); // filtered value
if(avgtotal > avgsum + (avgsum*0.01)){
// Serial.println(1);
if(prevState != 1){
peaks++;
prevState = 1;
}
}
else{
// Serial.println(0);
prevState = 0;
}
}
}
Serial.print("peaks:");
Serial.println(peaks);
}
So far I am getting somewhere close I guess. Down are some of the recorded measurements. Filtering the signal, I suspect, is not optimal to make clear distinction between peaks and trough in the signal. Also I am not sure if the shape of the signal is proper representation of the actual input signal given that the filtering influence that in a way or another based on the used parameters.
Any tips for what I should do and how I should proceed to make it more reliable and accurate?
Update 1:
Here is the Pi code:
import RPi.GPIO as GPIO # Import Raspberry Pi GPIO library
from time import sleep # Import the sleep function from the time module
GPIO.setwarnings(False) # Ignore warning for now
GPIO.setmode(GPIO.BOARD) # Use physical pin numbering
GPIO.setup(8, GPIO.OUT, initial=GPIO.LOW) # Set pin 8 to be an output pin and set initial value to low (off)
freq = 5.0 # Hz
cycle = 1.0/freq
hcycle = cycle / 2.0
while True: # Run forever
GPIO.output(8, GPIO.HIGH) # Turn on
sleep(hcycle) # Sleep for N second
GPIO.output(8, GPIO.LOW) # Turn off
sleep(hcycle)
Update 2:
After updating the code, for very low frequencies I get kinda nicely shaped-signal
example of 5 Hz signal enter image description here
Things start to get messier with high frequency (not that high) at 30 Hz for instance, where trying to segregate the signal based on the total average line does not result in informative value for how many peaks and troughs there are, for several peaks can be under the lines and several troughs can be above the line. Output reads 25 peaks here.
so I am starting to think that the filtering technique I am using maybe is not the best. Any suggestions? enter image description here
-
If you are seeing 1 extra peak at 15 Hz, and then 3 extra at 50 Hz (3x15 ~ 50), so at least it is consistent. You haven't posted your Pi code, which may or may not have an issue. Are you sure the Pi is flashing at 15 Hz and not 16 Hz? Maybe try 5 Hz to see if that is more accurate, and then slowly ramp up the rate. Have you tried flashing the LED using the Arduino (maybe by using interrupts) to see if the number of peaks change (or become more accurate)..?Greenonline– Greenonline03/26/2024 09:44:58Commented Mar 26, 2024 at 9:44
-
1I've just updated the question with some info, please check. I am very new to all this circuitry and I was not sure how to use the LED and the sensor simultaneously to measure the LED flashing as both would be sequential in the same loop(), so I decided to use another circuit to control the LED.wisdom– wisdom03/26/2024 10:08:32Commented Mar 26, 2024 at 10:08
-
Could you update the units on the figures? Are you sure you are taking measurements every second? Or what is your time-scale like?Nick S.– Nick S.03/26/2024 23:50:14Commented Mar 26, 2024 at 23:50
-
Why are you doing an analog read on what is basically a digital input? The LED is on, or off isn't it?Nick Gammon– Nick Gammon ♦03/27/2024 07:00:37Commented Mar 27, 2024 at 7:00
-
@NickGammon I am not interested in the LED per se, it is here only as a mean to test how to do proper reading using a photodiode sensor which has an analogue inputwisdom– wisdom03/27/2024 15:37:20Commented Mar 27, 2024 at 15:37
2 Answers 2
Counting in a loop like that is fine if you have a very low frequency signal, and are getting the hang of how loops, and reading from input pins, work. But there will be a lot of overheads. As Edgar Bonet points out in his answer there can be an overhead of between 104 μs and 112 μs for doing an analogRead call. Plus the overhead of the loop. And the delay won't be accurate to the microsecond. All this adds up.
There are two ways you can find a frequency:
Count "ticks" over a known interval. For example, counting 50 ticks over a second would give you 50 Hz.
Measure the period. In other words, find out how long elapses between a rising edge and a falling edge (and double that time). For example for 50 Hz the period would be 20 ms (1 / 50).
As I mention on my page about timers there are ways of doing that. To avoid giving a link-only answer I'll paste some of that code.
Count pulses
// Timer and Counter example
// Author: Nick Gammon
// Date: 17th January 2012
// Input: Pin D5
// these are checked for in the main program
volatile unsigned long timerCounts;
volatile boolean counterReady;
// internal to counting routine
unsigned long overflowCount;
unsigned int timerTicks;
unsigned int timerPeriod;
void startCounting (unsigned int ms)
{
counterReady = false; // time not up yet
timerPeriod = ms; // how many 1 ms counts to do
timerTicks = 0; // reset interrupt counter
overflowCount = 0; // no overflows yet
// reset Timer 1 and Timer 2
TCCR1A = 0;
TCCR1B = 0;
TCCR2A = 0;
TCCR2B = 0;
// Timer 1 - counts events on pin D5
TIMSK1 = bit (TOIE1); // interrupt on Timer 1 overflow
// Timer 2 - gives us our 1 ms counting interval
// 16 MHz clock (62.5 ns per tick) - prescaled by 128
// counter increments every 8 μs.
// So we count 125 of them, giving exactly 1000 μs (1 ms)
TCCR2A = bit (WGM21) ; // CTC mode
OCR2A = 124; // count up to 125 (zero relative!!!!)
// Timer 2 - interrupt on match (ie. every 1 ms)
TIMSK2 = bit (OCIE2A); // enable Timer2 Interrupt
TCNT1 = 0; // Both counters to zero
TCNT2 = 0;
// Reset prescalers
GTCCR = bit (PSRASY); // reset prescaler now
// start Timer 2
TCCR2B = bit (CS20) | bit (CS22) ; // prescaler of 128
// start Timer 1
// External clock source on T1 pin (D5). Clock on rising edge.
TCCR1B = bit (CS10) | bit (CS11) | bit (CS12);
} // end of startCounting
ISR (TIMER1_OVF_vect)
{
++overflowCount; // count number of Counter1 overflows
} // end of TIMER1_OVF_vect
//******************************************************************
// Timer2 Interrupt Service is invoked by hardware Timer 2 every 1 ms = 1000 Hz
// 16Mhz / 128 / 125 = 1000 Hz
ISR (TIMER2_COMPA_vect)
{
// grab counter value before it changes any more
unsigned int timer1CounterValue;
timer1CounterValue = TCNT1; // see datasheet, page 117 (accessing 16-bit registers)
unsigned long overflowCopy = overflowCount;
// see if we have reached timing period
if (++timerTicks < timerPeriod)
return; // not yet
// if just missed an overflow
if ((TIFR1 & bit (TOV1)) && timer1CounterValue < 256)
overflowCopy++;
// end of gate time, measurement ready
TCCR1A = 0; // stop timer 1
TCCR1B = 0;
TCCR2A = 0; // stop timer 2
TCCR2B = 0;
TIMSK1 = 0; // disable Timer1 Interrupt
TIMSK2 = 0; // disable Timer2 Interrupt
// calculate total count
timerCounts = (overflowCopy << 16) + timer1CounterValue; // each overflow is 65536 more
counterReady = true; // set global flag for end count period
} // end of TIMER2_COMPA_vect
void setup ()
{
Serial.begin(115200);
Serial.println("Frequency Counter");
} // end of setup
void loop ()
{
// stop Timer 0 interrupts from throwing the count out
byte oldTCCR0A = TCCR0A;
byte oldTCCR0B = TCCR0B;
TCCR0A = 0; // stop timer 0
TCCR0B = 0;
startCounting (500); // how many ms to count for
while (!counterReady)
{ } // loop until count over
// adjust counts by counting interval to give frequency in Hz
float frq = (timerCounts * 1000.0) / timerPeriod;
Serial.print ("Frequency: ");
Serial.print ((unsigned long) frq);
Serial.println (" Hz.");
// restart timer 0
TCCR0A = oldTCCR0A;
TCCR0B = oldTCCR0B;
// let serial stuff finish
delay(200);
} // end of loop
This is actually not so great for low frequencies because if the count is out by even one tick, your frequency is wrong by 2%. However it can count accurately (more or less) up to 5 kHz.
Measure the period
The other technique is to measure the period, like this:
// Frequency timer
// Author: Nick Gammon
// Date: 10th February 2012
// Input: Pin D2
volatile boolean first;
volatile boolean triggered;
volatile unsigned long overflowCount;
volatile unsigned long startTime;
volatile unsigned long finishTime;
// here on rising edge
void isr ()
{
unsigned int counter = TCNT1; // quickly save it
// wait until we noticed last one
if (triggered)
return;
if (first)
{
startTime = (overflowCount << 16) + counter;
first = false;
return;
}
finishTime = (overflowCount << 16) + counter;
triggered = true;
detachInterrupt(0);
} // end of isr
// timer overflows (every 65536 counts)
ISR (TIMER1_OVF_vect)
{
overflowCount++;
} // end of TIMER1_OVF_vect
void prepareForInterrupts ()
{
// get ready for next time
EIFR = bit (INTF0); // clear flag for interrupt 0
first = true;
triggered = false; // re-arm for next time
attachInterrupt(0, isr, RISING);
} // end of prepareForInterrupts
void setup ()
{
Serial.begin(115200);
Serial.println("Frequency Counter");
// reset Timer 1
TCCR1A = 0;
TCCR1B = 0;
// Timer 1 - interrupt on overflow
TIMSK1 = bit (TOIE1); // enable Timer1 Interrupt
// zero it
TCNT1 = 0;
overflowCount = 0;
// start Timer 1
TCCR1B = bit (CS10); // no prescaling
// set up for interrupts
prepareForInterrupts ();
} // end of setup
void loop ()
{
if (!triggered)
return;
unsigned long elapsedTime = finishTime - startTime;
float freq = F_CPU / float (elapsedTime); // each tick is 62.5 ns at 16 MHz
Serial.print ("Took: ");
Serial.print (elapsedTime);
Serial.print (" counts. ");
Serial.print ("Frequency: ");
Serial.print (freq);
Serial.println (" Hz. ");
// so we can read it
delay (500);
prepareForInterrupts ();
} // end of loop
This is better for low frequencies. Since this uses hardware timers you need to connect your input up to a specific pin as noted in the code. No doing analog reads, the time taken to do that would swamp any accuracy that you might get.
-
There is a race in your
isr()
function: if the timer overflows right before you readTCNT1
,TIMER1_OVF_vect
will not be called right away (because we are already in interrupt context), and you will miss an overflow.Edgar Bonet– Edgar Bonet03/27/2024 11:08:25Commented Mar 27, 2024 at 11:08 -
both codes work brilliantly for LED (though with margin of error up to 6% as I've noticed).... but maybe I was not too clear in my intentions that I strictly want to use a photodiode sensor to read frequencies off displays at the end, and I am using an LED for now just as a mean of testing and experimenting for how to design the code.wisdom– wisdom03/27/2024 15:19:08Commented Mar 27, 2024 at 15:19
-
also I am interested in the waveform for how the signal rise and fall regardless of what is the light source (e.g. PC monitor, LED, projector, ...etc.). So I need to use a photodiode for that as wellwisdom– wisdom03/27/2024 15:20:26Commented Mar 27, 2024 at 15:20
-
@EdgarBonet You mean in the second piece of posted code? It looks like you are right. I had the test in
TIMER2_COMPA_vect
in the first piece of code. You could add that test where it saysif ((TIFR1 & bit (TOV1)) && timer1CounterValue < 256) ...
into the second one.03/27/2024 20:50:35Commented Mar 27, 2024 at 20:50 -
@wisdom The measurements should be more accurate than 6%, possibly the test frequency you are sending it is not all that accurate. You are again using a loop and not a hardware generated frequency, so that the overhead of the loop is likely to affect the overall result. You really want a proper hardware-generated signal for testing, not code running on a processor using a
while
loop.03/27/2024 20:55:58Commented Mar 27, 2024 at 20:55
Consider this loop:
for(int i=0; i< size;i++){
// Read sensor value
sensorValue[i] = analogRead(sensorPin);
// wait between readings for N microseconds
delayMicroseconds(dly);
}
The time taken by each iteration is the sum of the times taken by each
individual operation (compare i
to size
, read the ADC, delay,
increment i
and jump to the start of the loop). Other than the delay,
the longest operation here is analogRead()
, as it blocks in a busy
loop while the ADC performs its conversion. This can take anywhere
between 104 μs and 112 μs. Even neglecting everything but
analogRead()
and delayMicroseconds()
, you expect your loop to be
about 6–7% too slow.
You should get better timings if you use micros()
instead of
delayMicroseconds()
. Check the Arduino tutorial "Blink without delay"
to see how you can manage your timings without delaying. You still won't
get metrology-class timings though, as micros()
is only as good as the
ceramic resonator clocking your Uno, which should be roughly 0.1%
accurate in its frequency. If you need anything better, you will have to
find a better frequency standard.
Edit: Addressing the updated question.
There are a few issues I would like to address:
First, the way the updated code handles the acquisition timing is a bit
convoluted. Furthermore, updating the time with prevTime = curTime;
is
prone to timing drift: you cannot count on curTime
being exactly the
time at which the ADC reading was scheduled, it can be a few
microseconds later. Actually, curTime
will always be later, as the
acquisition period (1,666 μs) is not a multiple of the micros()
resolution (4 μs). This timing drift can be avoided by making
prevTime
represent the time when the previous acquisition was
scheduled (not when it was performed):
// Data acquisition.
unsigned long prevTime = micros();
for (int i = 0; i < size; i++) {
while (micros() - prevTime < dly) continue; // wait until it's time
prevTime += dly; // update scheduled time
sensorValue[i] = analogRead(sensorPin); // acquire data
}
The limited resolution of micros()
will still cause some jitter, but
updating with prevTime += dly;
ensures there is no systematic drift.
Second, I would avoid doing any kind of filtering on the Arduino itself. If the data is meant to be transferred to the PC anyway, just transfer the raw data. This gives you the chance of processing it on your PC, interactively experimenting with different types of filters, with the comfort of your favorite programming language or data-processing package. If the complete project requires the filtering to be done on the Arduino, do that later, once you have found the most appropriate filter on the PC.
Third, as filtering goes, you seem to have a lot of 50 Hz noise here. Maybe the noise is already present in the luminous flux, or maybe it is introduced somewhere between the photodiode and the Arduino. I would check the cabling to see whether there is a way to reduce inductive noise pickup. Are you using long cables between the photodiode and the Arduino? If so, is that a twisted pair? Is everything properly grounded? Are there any ground loops?
If you cannot get rid of the mains noise at the source, it will be very difficult to identify signals that are anywhere close to 50 Hz, as your 30 Hz experiment shows. You may want to try a notch filter specifically tuned to kill any 50 Hz component.
Fourth, you may want to try, at least once, to acquire at a faster rate: let's say with a 200 μs period. If that acquisition reveals a lot of high-frequency noise, then it may be worth using some decimating filter. For example, you could store, in each array position, the sum of eight samples taken at 200 μs intervals. That should give less noisy data, with a period of 1.6 ms per array cell (round value close to the current 1.666 ms). Note that this technique will be absolutely useless against 50 Hz noise though.
Fifth, it is possible to trigger the ADC from a timer, which gives very accurate and consistent timing. This, however, requires low-level programming, and digging into the ATmega datasheet. I would not care about perfect timings now, at least not until the above issues are fixed. It is, however, a possibility you may want to keep in mind in case in the future you want to get the best possible timings.
-
thanks for the tip. I have just updated the code using the suggested method for capturing data within 1 second. But my main inquiry about how to process the recorded signal so to make informative reading out of it is still confusing mewisdom– wisdom03/27/2024 15:36:23Commented Mar 27, 2024 at 15:36
-
@wisdom This isn't really a forum, and making major edits to your question in response to answers is trying to turn it into one. I think your original question has been answered, and further questions about such things as processing the signal should really be turned into new questions in their own right. Otherwise this particular thread will become quite confusing to read. Amended questions / amended answers, don't make for a good reading flow.03/27/2024 20:59:47Commented Mar 27, 2024 at 20:59