0

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

asked Mar 26, 2024 at 9:29
8
  • 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)..? Commented Mar 26, 2024 at 9:44
  • 1
    I'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. Commented 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? Commented 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? Commented 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 input Commented Mar 27, 2024 at 15:37

2 Answers 2

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:

  1. Count "ticks" over a known interval. For example, counting 50 ticks over a second would give you 50 Hz.

  2. 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.

answered Mar 27, 2024 at 7:50
9
  • There is a race in your isr() function: if the timer overflows right before you read TCNT1, TIMER1_OVF_vect will not be called right away (because we are already in interrupt context), and you will miss an overflow. Commented 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. Commented 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 well Commented 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 says if ((TIFR1 & bit (TOV1)) && timer1CounterValue < 256) ... into the second one. Commented 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. Commented Mar 27, 2024 at 20:55
2

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.

answered Mar 26, 2024 at 10:18
2
  • 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 me Commented 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. Commented Mar 27, 2024 at 20:59

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.