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

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 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.
    – wisdom
    Commented Mar 26 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.
    Commented Mar 26 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
    Commented Mar 27 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
    – wisdom
    Commented Mar 27 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.

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 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
    Commented Mar 27 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
    – wisdom
    Commented Mar 27 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.
    – Nick Gammon
    Commented Mar 27 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.
    – Nick Gammon
    Commented Mar 27 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.

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
    – wisdom
    Commented Mar 27 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.
    – Nick Gammon
    Commented Mar 27 at 20:59

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.